OLD | NEW |
1 /* | 1 /* |
2 * This file is part of Adblock Plus <https://adblockplus.org/>, | 2 * This file is part of Adblock Plus <https://adblockplus.org/>, |
3 * Copyright (C) 2006-2017 eyeo GmbH | 3 * Copyright (C) 2006-2017 eyeo GmbH |
4 * | 4 * |
5 * Adblock Plus is free software: you can redistribute it and/or modify | 5 * Adblock Plus is free software: you can redistribute it and/or modify |
6 * it under the terms of the GNU General Public License version 3 as | 6 * it under the terms of the GNU General Public License version 3 as |
7 * published by the Free Software Foundation. | 7 * published by the Free Software Foundation. |
8 * | 8 * |
9 * Adblock Plus is distributed in the hope that it will be useful, | 9 * Adblock Plus is distributed in the hope that it will be useful, |
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of | 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
12 * GNU General Public License for more details. | 12 * GNU General Public License for more details. |
13 * | 13 * |
14 * You should have received a copy of the GNU General Public License | 14 * You should have received a copy of the GNU General Public License |
15 * along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. | 15 * along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. |
16 */ | 16 */ |
17 | 17 |
18 /* globals filterToRegExp */ | 18 /* globals filterToRegExp */ |
19 | 19 |
20 "use strict"; | 20 "use strict"; |
21 | 21 |
22 let propertySelectorRegExp = /\[-abp-properties=(["'])([^"']+)\1\]/; | 22 let propertySelectorRegExp = /\[-abp-properties=(["'])([^"']+)\1\]/; |
| 23 let pseudoClassHasSelectorRegExp = /:-abp-has\((.*)\)/; |
| 24 let pseudoClassPropsSelectorRegExp = /:-abp-properties\((["'])([^"']+)\1\)/; |
23 | 25 |
24 function splitSelector(selector) | 26 function splitSelector(selector) |
25 { | 27 { |
26 if (selector.indexOf(",") == -1) | 28 if (selector.indexOf(",") == -1) |
27 return [selector]; | 29 return [selector]; |
28 | 30 |
29 let selectors = []; | 31 let selectors = []; |
30 let start = 0; | 32 let start = 0; |
31 let level = 0; | 33 let level = 0; |
32 let sep = ""; | 34 let sep = ""; |
(...skipping 19 matching lines...) Expand all Loading... |
52 selectors.push(selector.substring(start, i)); | 54 selectors.push(selector.substring(start, i)); |
53 start = i + 1; | 55 start = i + 1; |
54 } | 56 } |
55 } | 57 } |
56 } | 58 } |
57 | 59 |
58 selectors.push(selector.substring(start)); | 60 selectors.push(selector.substring(start)); |
59 return selectors; | 61 return selectors; |
60 } | 62 } |
61 | 63 |
62 function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc) | 64 // 1 base index like for :nth-child() |
| 65 function positionInParent(node) |
| 66 { |
| 67 let parentNode = node ? node.parentNode : null; |
| 68 if (parentNode == null) |
| 69 return 0; |
| 70 |
| 71 let {children} = parentNode; |
| 72 if (!children) |
| 73 return 0; |
| 74 let i = 0; |
| 75 for (i = 0; i < children.length; i++) |
| 76 if (children[i] == node) |
| 77 break; |
| 78 return i + 1; |
| 79 } |
| 80 |
| 81 function makeSelector(node, selector) |
| 82 { |
| 83 let idx = positionInParent(node); |
| 84 if (idx > 0) |
| 85 { |
| 86 let newSelector = `:nth-child(${idx}) `; |
| 87 if (selector != "") |
| 88 newSelector += "> "; |
| 89 return makeSelector(node.parentNode, newSelector + selector); |
| 90 } |
| 91 |
| 92 return selector; |
| 93 } |
| 94 |
| 95 function matchChildren(e, selector) |
| 96 { |
| 97 let newSelector = makeSelector(e, ""); |
| 98 newSelector += selector; |
| 99 |
| 100 return document.querySelector(newSelector) != null; |
| 101 } |
| 102 |
| 103 function selectChildren(e, selector) |
| 104 { |
| 105 let newSelector = makeSelector(e, ""); |
| 106 newSelector += selector; |
| 107 |
| 108 return document.querySelectorAll(newSelector); |
| 109 } |
| 110 |
| 111 function parsePattern(pattern) |
| 112 { |
| 113 let {selector} = pattern; |
| 114 let match = pseudoClassHasSelectorRegExp.exec(selector); |
| 115 if (match) |
| 116 return { |
| 117 type: "has", |
| 118 text: pattern.text, |
| 119 elementMatcher: new PseudoHasMatcher(match[1]), |
| 120 prefix: selector.substr(0, match.index).trim(), |
| 121 suffix: selector.substr(match.index + match[0].length).trim() |
| 122 }; |
| 123 match = pseudoClassPropsSelectorRegExp.exec(selector); |
| 124 |
| 125 if (!match) |
| 126 match = propertySelectorRegExp.exec(selector); |
| 127 if (match) |
| 128 { |
| 129 let regexpString; |
| 130 let propertyExpression = match[2]; |
| 131 if (propertyExpression.length >= 2 && propertyExpression[0] == "/" && |
| 132 propertyExpression[propertyExpression.length - 1] == "/") |
| 133 regexpString = propertyExpression.slice(1, -1) |
| 134 .replace("\\x7B ", "{").replace("\\x7D ", "}"); |
| 135 else |
| 136 regexpString = filterToRegExp(propertyExpression); |
| 137 return { |
| 138 type: "props", |
| 139 text: pattern.text, |
| 140 regexp: new RegExp(regexpString, "i"), |
| 141 prefix: selector.substr(0, match.index), |
| 142 suffix: selector.substr(match.index + match[0].length) |
| 143 }; |
| 144 } |
| 145 } |
| 146 |
| 147 function matchStyleProps(style, rule, pattern, selectors, filters) |
| 148 { |
| 149 if (pattern.regexp.test(style)) |
| 150 { |
| 151 let subSelectors = splitSelector(rule.selectorText); |
| 152 for (let i = 0; i < subSelectors.length; i++) |
| 153 { |
| 154 let subSelector = subSelectors[i]; |
| 155 selectors.push(pattern.prefix + subSelector + pattern.suffix); |
| 156 filters.push(pattern.text); |
| 157 } |
| 158 } |
| 159 } |
| 160 |
| 161 function findPropsSelectors(stylesheet, patterns, selectors, filters) |
| 162 { |
| 163 let rules = stylesheet.cssRules; |
| 164 if (!rules) |
| 165 return; |
| 166 |
| 167 for (let rule of rules) |
| 168 { |
| 169 if (rule.type != rule.STYLE_RULE) |
| 170 continue; |
| 171 |
| 172 let style = stringifyStyle(rule.style); |
| 173 for (let pattern of patterns) |
| 174 { |
| 175 matchStyleProps(style, rule, pattern, selectors, filters); |
| 176 } |
| 177 } |
| 178 } |
| 179 |
| 180 /** |
| 181 * Match the selector @pattern containing :-abp-has() starting from @node. |
| 182 * @param {string} pattern - the pattern to match |
| 183 * @param {object} node - the top level node. |
| 184 * @param {object} stylesheets - the stylesheets to check from. |
| 185 * @param {array} elements - elements that match |
| 186 * @param {array} filters - filters that match |
| 187 */ |
| 188 function pseudoClassHasMatch(pattern, node, stylesheets, elements, filters) |
| 189 { |
| 190 // select element for the prefix pattern. Or just use node. |
| 191 let haveElems = pattern.prefix ? node.querySelectorAll(pattern.prefix) : |
| 192 [node]; |
| 193 for (let elem of haveElems) |
| 194 { |
| 195 let matched = pattern.elementMatcher.match(elem, stylesheets); |
| 196 if (!matched) |
| 197 continue; |
| 198 |
| 199 if (pattern.suffix) |
| 200 { |
| 201 let subElements = selectChildren(elem, pattern.suffix); |
| 202 if (subElements) |
| 203 { |
| 204 for (let subElement of subElements) |
| 205 { |
| 206 elements.push(subElement); |
| 207 filters.push(pattern.text); |
| 208 } |
| 209 } |
| 210 } |
| 211 else |
| 212 { |
| 213 elements.push(elem); |
| 214 filters.push(pattern.text); |
| 215 } |
| 216 } |
| 217 } |
| 218 |
| 219 function stringifyStyle(style) |
| 220 { |
| 221 let styles = []; |
| 222 for (let i = 0; i < style.length; i++) |
| 223 { |
| 224 let property = style.item(i); |
| 225 let value = style.getPropertyValue(property); |
| 226 let priority = style.getPropertyPriority(property); |
| 227 styles.push(property + ": " + value + (priority ? " !" + priority : "") + |
| 228 ";"); |
| 229 } |
| 230 styles.sort(); |
| 231 return styles.join(" "); |
| 232 } |
| 233 |
| 234 /** matcher for the pseudo class :-abp-has() |
| 235 * For those browser that don't have it yet. |
| 236 * @param {string} selector - the inner selector. |
| 237 */ |
| 238 function PseudoHasMatcher(selector) |
| 239 { |
| 240 this.hasSelector = selector; |
| 241 this.parsed = parsePattern({selector}); |
| 242 } |
| 243 |
| 244 PseudoHasMatcher.prototype = { |
| 245 match(elem, stylesheets) |
| 246 { |
| 247 let selectors = []; |
| 248 |
| 249 if (this.parsed) |
| 250 { |
| 251 let filters = []; // don't need this, we have a partial filter. |
| 252 if (this.parsed.type == "has") |
| 253 { |
| 254 let child = elem.firstChild; |
| 255 while (child) |
| 256 { |
| 257 if (child.nodeType === Node.ELEMENT_NODE) |
| 258 { |
| 259 let matches = []; |
| 260 pseudoClassHasMatch(this.parsed, child, stylesheets, matches, |
| 261 filters); |
| 262 if (matches.length > 0) |
| 263 return true; |
| 264 } |
| 265 child = child.nextSibling; |
| 266 } |
| 267 return false; |
| 268 } |
| 269 if (this.parsed.type == "props") |
| 270 for (let stylesheet of stylesheets) |
| 271 findPropsSelectors(stylesheet, [this.parsed], selectors, filters); |
| 272 } |
| 273 else |
| 274 selectors = [this.hasSelector]; |
| 275 |
| 276 let matched = false; |
| 277 // look up for all elements that match the :-abp-has(). |
| 278 for (let selector of selectors) |
| 279 try |
| 280 { |
| 281 matched = matchChildren(elem, selector); |
| 282 if (matched) |
| 283 break; |
| 284 } |
| 285 catch (e) |
| 286 { |
| 287 console.error("Exception with querySelector()", selector, e); |
| 288 } |
| 289 |
| 290 return matched; |
| 291 } |
| 292 }; |
| 293 |
| 294 function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc, |
| 295 hideElementsFunc) |
63 { | 296 { |
64 this.window = window; | 297 this.window = window; |
65 this.getFiltersFunc = getFiltersFunc; | 298 this.getFiltersFunc = getFiltersFunc; |
66 this.addSelectorsFunc = addSelectorsFunc; | 299 this.addSelectorsFunc = addSelectorsFunc; |
| 300 this.hideElementsFunc = hideElementsFunc; |
67 } | 301 } |
68 | 302 |
69 ElemHideEmulation.prototype = { | 303 ElemHideEmulation.prototype = { |
70 stringifyStyle(style) | |
71 { | |
72 let styles = []; | |
73 for (let i = 0; i < style.length; i++) | |
74 { | |
75 let property = style.item(i); | |
76 let value = style.getPropertyValue(property); | |
77 let priority = style.getPropertyPriority(property); | |
78 styles.push(property + ": " + value + (priority ? " !" + priority : "") + | |
79 ";"); | |
80 } | |
81 styles.sort(); | |
82 return styles.join(" "); | |
83 }, | |
84 | 304 |
85 isSameOrigin(stylesheet) | 305 isSameOrigin(stylesheet) |
86 { | 306 { |
87 try | 307 try |
88 { | 308 { |
89 return new URL(stylesheet.href).origin == this.window.location.origin; | 309 return new URL(stylesheet.href).origin == this.window.location.origin; |
90 } | 310 } |
91 catch (e) | 311 catch (e) |
92 { | 312 { |
93 // Invalid URL, assume that it is first-party. | 313 // Invalid URL, assume that it is first-party. |
94 return true; | 314 return true; |
95 } | 315 } |
96 }, | 316 }, |
97 | 317 |
98 findSelectors(stylesheet, selectors, filters) | 318 findPseudoClassHasElements(node, stylesheets, elements, filters) |
99 { | 319 { |
100 // Explicitly ignore third-party stylesheets to ensure consistent behavior | 320 for (let pattern of this.pseudoHasPatterns) |
101 // between Firefox and Chrome. | 321 pseudoClassHasMatch(pattern, node, stylesheets, elements, filters); |
102 if (!this.isSameOrigin(stylesheet)) | |
103 return; | |
104 | |
105 let rules = stylesheet.cssRules; | |
106 if (!rules) | |
107 return; | |
108 | |
109 for (let rule of rules) | |
110 { | |
111 if (rule.type != rule.STYLE_RULE) | |
112 continue; | |
113 | |
114 let style = this.stringifyStyle(rule.style); | |
115 for (let pattern of this.patterns) | |
116 { | |
117 if (pattern.regexp.test(style)) | |
118 { | |
119 let subSelectors = splitSelector(rule.selectorText); | |
120 for (let subSelector of subSelectors) | |
121 { | |
122 selectors.push(pattern.prefix + subSelector + pattern.suffix); | |
123 filters.push(pattern.text); | |
124 } | |
125 } | |
126 } | |
127 } | |
128 }, | 322 }, |
129 | 323 |
130 addSelectors(stylesheets) | 324 addSelectors(stylesheets) |
131 { | 325 { |
132 let selectors = []; | 326 let selectors = []; |
133 let filters = []; | 327 let filters = []; |
134 for (let stylesheet of stylesheets) | 328 for (let stylesheet of stylesheets) |
135 this.findSelectors(stylesheet, selectors, filters); | 329 { |
| 330 // Explicitly ignore third-party stylesheets to ensure consistent behavior |
| 331 // between Firefox and Chrome. |
| 332 if (!this.isSameOrigin(stylesheet)) |
| 333 continue; |
| 334 findPropsSelectors(stylesheet, this.propSelPatterns, selectors, filters); |
| 335 } |
136 this.addSelectorsFunc(selectors, filters); | 336 this.addSelectorsFunc(selectors, filters); |
137 }, | 337 }, |
138 | 338 |
| 339 hideElements(stylesheets) |
| 340 { |
| 341 let elements = []; |
| 342 let filters = []; |
| 343 this.findPseudoClassHasElements(document, stylesheets, elements, filters); |
| 344 this.hideElementsFunc(elements, filters); |
| 345 }, |
| 346 |
139 onLoad(event) | 347 onLoad(event) |
140 { | 348 { |
141 let stylesheet = event.target.sheet; | 349 let stylesheet = event.target.sheet; |
142 if (stylesheet) | 350 if (stylesheet) |
143 this.addSelectors([stylesheet]); | 351 this.addSelectors([stylesheet]); |
| 352 this.hideElements([stylesheet]); |
144 }, | 353 }, |
145 | 354 |
146 apply() | 355 apply() |
147 { | 356 { |
148 this.getFiltersFunc(patterns => | 357 this.getFiltersFunc(patterns => |
149 { | 358 { |
150 this.patterns = []; | 359 this.propSelPatterns = []; |
| 360 this.pseudoHasPatterns = []; |
| 361 |
151 for (let pattern of patterns) | 362 for (let pattern of patterns) |
152 { | 363 { |
153 let match = propertySelectorRegExp.exec(pattern.selector); | 364 let parsed = parsePattern(pattern); |
154 if (!match) | 365 if (parsed == undefined) |
155 continue; | 366 continue; |
156 | 367 |
157 let propertyExpression = match[2]; | 368 if (parsed.type == "props") |
158 let regexpString; | |
159 if (propertyExpression.length >= 2 && propertyExpression[0] == "/" && | |
160 propertyExpression[propertyExpression.length - 1] == "/") | |
161 { | 369 { |
162 regexpString = propertyExpression.slice(1, -1) | 370 this.propSelPatterns.push(parsed); |
163 .replace("\\x7B ", "{").replace("\\x7D ", "}"); | |
164 } | 371 } |
165 else | 372 else if (parsed.type == "has") |
166 regexpString = filterToRegExp(propertyExpression); | 373 { |
167 | 374 this.pseudoHasPatterns.push(parsed); |
168 this.patterns.push({ | 375 } |
169 text: pattern.text, | 376 } |
170 regexp: new RegExp(regexpString, "i"), | 377 |
171 prefix: pattern.selector.substr(0, match.index), | 378 if (this.pseudoHasPatterns.length > 0 || this.propSelPatterns.length > 0) |
172 suffix: pattern.selector.substr(match.index + match[0].length) | |
173 }); | |
174 } | |
175 | |
176 if (this.patterns.length > 0) | |
177 { | 379 { |
178 let {document} = this.window; | 380 let {document} = this.window; |
179 this.addSelectors(document.styleSheets); | 381 this.addSelectors(document.styleSheets); |
| 382 this.hideElements(document.styleSheets); |
180 document.addEventListener("load", this.onLoad.bind(this), true); | 383 document.addEventListener("load", this.onLoad.bind(this), true); |
181 } | 384 } |
182 }); | 385 }); |
183 } | 386 } |
184 }; | 387 }; |
OLD | NEW |