| 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 | 
| (...skipping 41 matching lines...) Expand 10 before | Expand all | Expand 10 after  Loading... | 
| 52         selectors.push(selector.substring(start, i)); | 52         selectors.push(selector.substring(start, i)); | 
| 53         start = i + 1; | 53         start = i + 1; | 
| 54       } | 54       } | 
| 55     } | 55     } | 
| 56   } | 56   } | 
| 57 | 57 | 
| 58   selectors.push(selector.substring(start)); | 58   selectors.push(selector.substring(start)); | 
| 59   return selectors; | 59   return selectors; | 
| 60 } | 60 } | 
| 61 | 61 | 
| 62 function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc) | 62 /** Return position of node from parent. | 
|  | 63  * @param {Node} node the node to find the position of. | 
|  | 64  * @return {number} One-based index like for :nth-child(), or 0 on error. | 
|  | 65  */ | 
|  | 66 function positionInParent(node) | 
|  | 67 { | 
|  | 68   let {children} = node.parentNode; | 
|  | 69   for (let i = 0; i < children.length; i++) | 
|  | 70     if (children[i] == node) | 
|  | 71       return i + 1; | 
|  | 72   return 0; | 
|  | 73 } | 
|  | 74 | 
|  | 75 function makeSelector(node, selector) | 
|  | 76 { | 
|  | 77   if (!node.parentElement) | 
|  | 78   { | 
|  | 79     let newSelector = ":root"; | 
|  | 80     if (selector) | 
|  | 81       newSelector += " > " + selector; | 
|  | 82     return newSelector; | 
|  | 83   } | 
|  | 84   let idx = positionInParent(node); | 
|  | 85   if (idx > 0) | 
|  | 86   { | 
|  | 87     let newSelector = `${node.tagName}:nth-child(${idx})`; | 
|  | 88     if (selector) | 
|  | 89       newSelector += " > " + selector; | 
|  | 90     return makeSelector(node.parentElement, newSelector); | 
|  | 91   } | 
|  | 92 | 
|  | 93   return selector; | 
|  | 94 } | 
|  | 95 | 
|  | 96 function parseSelectorContent(content, startIndex) | 
|  | 97 { | 
|  | 98   let parens = 1; | 
|  | 99   let quote = null; | 
|  | 100   let i = startIndex; | 
|  | 101   for (; i < content.length; i++) | 
|  | 102   { | 
|  | 103     let c = content[i]; | 
|  | 104     if (c == "\\") | 
|  | 105     { | 
|  | 106       // Ignore escaped characters | 
|  | 107       i++; | 
|  | 108     } | 
|  | 109     else if (quote) | 
|  | 110     { | 
|  | 111       if (c == quote) | 
|  | 112         quote = null; | 
|  | 113     } | 
|  | 114     else if (c == "'" || c == '"') | 
|  | 115       quote = c; | 
|  | 116     else if (c == "(") | 
|  | 117       parens++; | 
|  | 118     else if (c == ")") | 
|  | 119     { | 
|  | 120       parens--; | 
|  | 121       if (parens == 0) | 
|  | 122         break; | 
|  | 123     } | 
|  | 124   } | 
|  | 125 | 
|  | 126   if (parens > 0) | 
|  | 127     return null; | 
|  | 128   return {text: content.substr(startIndex, i - startIndex), end: i}; | 
|  | 129 } | 
|  | 130 | 
|  | 131 /** Parse the selector | 
|  | 132  * @param {string} selector the selector to parse | 
|  | 133  * @return {Object} selectors is an array of objects, | 
|  | 134  * or null in case of errors. hide is true if we'll hide | 
|  | 135  * elements instead of styles.. | 
|  | 136  */ | 
|  | 137 function parseSelector(selector) | 
|  | 138 { | 
|  | 139   if (selector.length == 0) | 
|  | 140     return {selectors: [], hide: false}; | 
|  | 141 | 
|  | 142   let match = abpSelectorRegexp.exec(selector); | 
|  | 143   if (!match) | 
|  | 144     return {selectors: [new PlainSelector(selector)], hide: false}; | 
|  | 145 | 
|  | 146   let hide = false; | 
|  | 147   let selectors = []; | 
|  | 148   if (match.index > 0) | 
|  | 149     selectors.push(new PlainSelector(selector.substr(0, match.index))); | 
|  | 150 | 
|  | 151   let startIndex = match.index + match[0].length; | 
|  | 152   let content = parseSelectorContent(selector, startIndex); | 
|  | 153   if (content == null) | 
|  | 154   { | 
|  | 155     console.error(new SyntaxError("Failed parsing AdBlock Plus " + | 
|  | 156                                   `selector ${selector}, didn't ` + | 
|  | 157                                   "find closing parenthesis.")); | 
|  | 158     return {selectors: null, hide: false}; | 
|  | 159   } | 
|  | 160   if (match[1] == "properties") | 
|  | 161     selectors.push(new PropsSelector(content.text)); | 
|  | 162   else if (match[1] == "has") | 
|  | 163   { | 
|  | 164     let hasSelector = new HasSelector(content.text); | 
|  | 165     if (!hasSelector.valid()) | 
|  | 166       return {selectors: null, hide: false}; | 
|  | 167     selectors.push(hasSelector); | 
|  | 168     hide = true; | 
|  | 169   } | 
|  | 170   else | 
|  | 171   { | 
|  | 172     // this is an error, can't parse selector. | 
|  | 173     console.error(new SyntaxError("Failed parsing AdBlock Plus " + | 
|  | 174                                   `selector ${selector}, invalid ` + | 
|  | 175                                   `pseudo-class -abp-${match[1]}().`)); | 
|  | 176     return {selectors: null, hide: false}; | 
|  | 177   } | 
|  | 178 | 
|  | 179   let suffix = parseSelector(selector.substr(content.end + 1)); | 
|  | 180   if (suffix.selectors == null) | 
|  | 181     return {selectors: null, hide: false}; | 
|  | 182 | 
|  | 183   selectors.push(...suffix.selectors); | 
|  | 184   hide = hide || suffix.hide; | 
|  | 185 | 
|  | 186   return {selectors, hide}; | 
|  | 187 } | 
|  | 188 | 
|  | 189 function stringifyStyle(style) | 
|  | 190 { | 
|  | 191   let styles = []; | 
|  | 192   for (let i = 0; i < style.length; i++) | 
|  | 193   { | 
|  | 194     let property = style.item(i); | 
|  | 195     let value = style.getPropertyValue(property); | 
|  | 196     let priority = style.getPropertyPriority(property); | 
|  | 197     styles.push(property + ": " + value + (priority ? " !" + priority : "") + | 
|  | 198                 ";"); | 
|  | 199   } | 
|  | 200   styles.sort(); | 
|  | 201   return styles.join(" "); | 
|  | 202 } | 
|  | 203 | 
|  | 204 function* evaluate(chain, index, prefix, subtree, styles) | 
|  | 205 { | 
|  | 206   if (index >= chain.length) | 
|  | 207   { | 
|  | 208     yield prefix; | 
|  | 209     return; | 
|  | 210   } | 
|  | 211   for (let [selector, element] of | 
|  | 212        chain[index].getSelectors(prefix, subtree, styles)) | 
|  | 213     yield* evaluate(chain, index + 1, selector, element, styles); | 
|  | 214 } | 
|  | 215 | 
|  | 216 function PlainSelector(selector) | 
|  | 217 { | 
|  | 218   this._selector = selector; | 
|  | 219 } | 
|  | 220 | 
|  | 221 PlainSelector.prototype = { | 
|  | 222   /** | 
|  | 223    * Generator function returning a pair of selector | 
|  | 224    * string and subtree. | 
|  | 225    * @param {string} prefix the prefix for the selector. | 
|  | 226    * @param {Node} subtree the subtree we work on. | 
|  | 227    * @param {...StringifiedStyle} styles the stringified style objects. | 
|  | 228    */ | 
|  | 229   *getSelectors(prefix, subtree, styles) | 
|  | 230   { | 
|  | 231     yield [prefix + this._selector, subtree]; | 
|  | 232   } | 
|  | 233 }; | 
|  | 234 | 
|  | 235 const incompletePrefixRegexp = /[\s>+~]$/; | 
|  | 236 | 
|  | 237 function HasSelector(selector) | 
|  | 238 { | 
|  | 239   let inner = parseSelector(selector); | 
|  | 240   this._innerSelectors = inner ? inner.selectors : null; | 
|  | 241 } | 
|  | 242 | 
|  | 243 HasSelector.prototype = { | 
|  | 244   valid() | 
|  | 245   { | 
|  | 246     return this._innerSelectors != null; | 
|  | 247   }, | 
|  | 248 | 
|  | 249   *getSelectors(prefix, subtree, styles) | 
|  | 250   { | 
|  | 251     for (let element of this.getElements(prefix, subtree, styles)) | 
|  | 252       yield [makeSelector(element, ""), element]; | 
|  | 253   }, | 
|  | 254 | 
|  | 255   /** | 
|  | 256    * Generator function returning selected elements. | 
|  | 257    * @param {string} prefix the prefix for the selector. | 
|  | 258    * @param {Node} subtree the subtree we work on. | 
|  | 259    * @param {...StringifiedStyle} styles the stringified style objects. | 
|  | 260    */ | 
|  | 261   *getElements(prefix, subtree, styles) | 
|  | 262   { | 
|  | 263     let actualPrefix = (!prefix || incompletePrefixRegexp.test(prefix)) ? | 
|  | 264         prefix + "*" : prefix; | 
|  | 265     let elements = subtree.querySelectorAll(actualPrefix); | 
|  | 266     for (let element of elements) | 
|  | 267     { | 
|  | 268       let newPrefix = makeSelector(element, ""); | 
|  | 269       let iter = evaluate(this._innerSelectors, 0, newPrefix + " ", | 
|  | 270                           element, styles); | 
|  | 271       for (let selector of iter) | 
|  | 272         // we insert a space between the two. It becomes a no-op if selector | 
|  | 273         // doesn't have a combinator | 
|  | 274         if (subtree.querySelector(selector)) | 
|  | 275           yield element; | 
|  | 276     } | 
|  | 277   } | 
|  | 278 }; | 
|  | 279 | 
|  | 280 function PropsSelector(propertyExpression) | 
|  | 281 { | 
|  | 282   let regexpString; | 
|  | 283   if (propertyExpression.length >= 2 && propertyExpression[0] == "/" && | 
|  | 284       propertyExpression[propertyExpression.length - 1] == "/") | 
|  | 285   { | 
|  | 286     regexpString = propertyExpression.slice(1, -1) | 
|  | 287       .replace("\\x7B ", "{").replace("\\x7D ", "}"); | 
|  | 288   } | 
|  | 289   else | 
|  | 290     regexpString = filterToRegExp(propertyExpression); | 
|  | 291 | 
|  | 292   this._regexp = new RegExp(regexpString, "i"); | 
|  | 293 } | 
|  | 294 | 
|  | 295 PropsSelector.prototype = { | 
|  | 296   *findPropsSelectors(styles, prefix, regexp) | 
|  | 297   { | 
|  | 298     for (let style of styles) | 
|  | 299       if (regexp.test(style.style)) | 
|  | 300         for (let subSelector of style.subSelectors) | 
|  | 301           yield prefix + subSelector; | 
|  | 302   }, | 
|  | 303 | 
|  | 304   *getSelectors(prefix, subtree, styles) | 
|  | 305   { | 
|  | 306     for (let selector of this.findPropsSelectors(styles, prefix, this._regexp)) | 
|  | 307       yield [selector, subtree]; | 
|  | 308   } | 
|  | 309 }; | 
|  | 310 | 
|  | 311 function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc, | 
|  | 312                            hideElemsFunc) | 
| 63 { | 313 { | 
| 64   this.window = window; | 314   this.window = window; | 
| 65   this.getFiltersFunc = getFiltersFunc; | 315   this.getFiltersFunc = getFiltersFunc; | 
| 66   this.addSelectorsFunc = addSelectorsFunc; | 316   this.addSelectorsFunc = addSelectorsFunc; | 
|  | 317   this.hideElemsFunc = hideElemsFunc; | 
| 67 } | 318 } | 
| 68 | 319 | 
| 69 ElemHideEmulation.prototype = { | 320 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 |  | 
| 85   isSameOrigin(stylesheet) | 321   isSameOrigin(stylesheet) | 
| 86   { | 322   { | 
| 87     try | 323     try | 
| 88     { | 324     { | 
| 89       return new URL(stylesheet.href).origin == this.window.location.origin; | 325       return new URL(stylesheet.href).origin == this.window.location.origin; | 
| 90     } | 326     } | 
| 91     catch (e) | 327     catch (e) | 
| 92     { | 328     { | 
| 93       // Invalid URL, assume that it is first-party. | 329       // Invalid URL, assume that it is first-party. | 
| 94       return true; | 330       return true; | 
| 95     } | 331     } | 
| 96   }, | 332   }, | 
| 97 | 333 | 
| 98   findSelectors(stylesheet, selectors, filters) | 334   addSelectors(stylesheets) | 
| 99   { | 335   { | 
| 100     // Explicitly ignore third-party stylesheets to ensure consistent behavior | 336     let selectors = []; | 
| 101     // between Firefox and Chrome. | 337     let selectorFilters = []; | 
| 102     if (!this.isSameOrigin(stylesheet)) | 338 | 
| 103       return; | 339     let elements = []; | 
| 104 | 340     let elementFilters = []; | 
| 105     let rules = stylesheet.cssRules; | 341 | 
| 106     if (!rules) | 342     let cssStyles = []; | 
| 107       return; | 343 | 
| 108 | 344     for (let stylesheet of stylesheets) | 
| 109     for (let rule of rules) | 345     { | 
| 110     { | 346       // Explicitly ignore third-party stylesheets to ensure consistent behavior | 
| 111       if (rule.type != rule.STYLE_RULE) | 347       // between Firefox and Chrome. | 
|  | 348       if (!this.isSameOrigin(stylesheet)) | 
| 112         continue; | 349         continue; | 
| 113 | 350 | 
| 114       let style = this.stringifyStyle(rule.style); | 351       let rules = stylesheet.cssRules; | 
| 115       for (let pattern of this.patterns) | 352       if (!rules) | 
|  | 353         continue; | 
|  | 354 | 
|  | 355       for (let rule of rules) | 
| 116       { | 356       { | 
| 117         if (pattern.regexp.test(style)) | 357         if (rule.type != rule.STYLE_RULE) | 
|  | 358           continue; | 
|  | 359 | 
|  | 360         /* Stringified style objects | 
|  | 361          * @typedef {Object} StringifiedStyle | 
|  | 362          * @property {string} style stringified style | 
|  | 363          * @property {...string} subSelectors associated selectors | 
|  | 364          */ | 
|  | 365 | 
|  | 366         let style = stringifyStyle(rule.style); | 
|  | 367         let subSelectors = splitSelector(rule.selectorText); | 
|  | 368         cssStyles.push({style, subSelectors}); | 
|  | 369       } | 
|  | 370     } | 
|  | 371 | 
|  | 372     for (let pattern of this.patterns) | 
|  | 373     { | 
|  | 374       for (let selector of evaluate(pattern.selectors, | 
|  | 375                                     0, "", document, cssStyles)) | 
|  | 376       { | 
|  | 377         if (!pattern.hide) | 
| 118         { | 378         { | 
| 119           let subSelectors = splitSelector(rule.selectorText); | 379           selectors.push(selector); | 
| 120           for (let subSelector of subSelectors) | 380           selectorFilters.push(pattern.text); | 
|  | 381         } | 
|  | 382         else | 
|  | 383         { | 
|  | 384           for (let element of document.querySelectorAll(selector)) | 
| 121           { | 385           { | 
| 122             selectors.push(pattern.prefix + subSelector + pattern.suffix); | 386             elements.push(element); | 
| 123             filters.push(pattern.text); | 387             elementFilters.push(pattern.text); | 
| 124           } | 388           } | 
| 125         } | 389         } | 
| 126       } | 390       } | 
| 127     } | 391     } | 
| 128   }, | 392 | 
| 129 | 393     this.addSelectorsFunc(selectors, selectorFilters); | 
| 130   addSelectors(stylesheets) | 394     this.hideElemsFunc(elements, elementFilters); | 
| 131   { |  | 
| 132     let selectors = []; |  | 
| 133     let filters = []; |  | 
| 134     for (let stylesheet of stylesheets) |  | 
| 135       this.findSelectors(stylesheet, selectors, filters); |  | 
| 136     this.addSelectorsFunc(selectors, filters); |  | 
| 137   }, | 395   }, | 
| 138 | 396 | 
| 139   onLoad(event) | 397   onLoad(event) | 
| 140   { | 398   { | 
| 141     let stylesheet = event.target.sheet; | 399     let stylesheet = event.target.sheet; | 
| 142     if (stylesheet) | 400     if (stylesheet) | 
| 143       this.addSelectors([stylesheet]); | 401       this.addSelectors([stylesheet]); | 
| 144   }, | 402   }, | 
| 145 | 403 | 
| 146   apply() | 404   apply() | 
| 147   { | 405   { | 
| 148     this.getFiltersFunc(patterns => | 406     this.getFiltersFunc(patterns => | 
| 149     { | 407     { | 
| 150       this.patterns = []; | 408       this.patterns = []; | 
| 151       for (let pattern of patterns) | 409       for (let pattern of patterns) | 
| 152       { | 410       { | 
| 153         let match = abpSelectorRegexp.exec(pattern.selector); | 411         let {selectors, hide} = parseSelector(pattern.selector); | 
| 154         if (!match || match[1] != "properties") | 412         if (selectors != null && selectors.length > 0) | 
| 155         { | 413           this.patterns.push({selectors, hide, text: pattern.text}); | 
| 156           console.error(new SyntaxError( |  | 
| 157             `Failed to parse Adblock Plus selector ${pattern.selector}, ` + |  | 
| 158             `invalid pseudo-class :-abp-${match[1]}().` |  | 
| 159           )); |  | 
| 160           continue; |  | 
| 161         } |  | 
| 162 |  | 
| 163         let expressionStart = match.index + match[0].length; |  | 
| 164         let parens = 1; |  | 
| 165         let quote = null; |  | 
| 166         let i; |  | 
| 167         for (i = expressionStart; i < pattern.selector.length; i++) |  | 
| 168         { |  | 
| 169           let c = pattern.selector[i]; |  | 
| 170           if (c == "\\") |  | 
| 171           { |  | 
| 172             // Ignore escaped characters |  | 
| 173             i++; |  | 
| 174           } |  | 
| 175           else if (quote) |  | 
| 176           { |  | 
| 177             if (c == quote) |  | 
| 178               quote = null; |  | 
| 179           } |  | 
| 180           else if (c == "'" || c == '"') |  | 
| 181             quote = c; |  | 
| 182           else if (c == "(") |  | 
| 183             parens++; |  | 
| 184           else if (c == ")") |  | 
| 185           { |  | 
| 186             parens--; |  | 
| 187             if (parens == 0) |  | 
| 188               break; |  | 
| 189           } |  | 
| 190         } |  | 
| 191 |  | 
| 192         if (parens > 0) |  | 
| 193         { |  | 
| 194           console.error(new SyntaxError( |  | 
| 195             `Failed to parse Adblock Plus selector ${pattern.selector} ` + |  | 
| 196             "due to unmatched parentheses." |  | 
| 197           )); |  | 
| 198           continue; |  | 
| 199         } |  | 
| 200 |  | 
| 201         let propertyExpression = pattern.selector.substring(expressionStart, i); |  | 
| 202         let regexpString; |  | 
| 203         if (propertyExpression.length >= 2 && propertyExpression[0] == "/" && |  | 
| 204             propertyExpression[propertyExpression.length - 1] == "/") |  | 
| 205         { |  | 
| 206           regexpString = propertyExpression.slice(1, -1) |  | 
| 207               .replace("\\x7B ", "{").replace("\\x7D ", "}"); |  | 
| 208         } |  | 
| 209         else |  | 
| 210           regexpString = filterToRegExp(propertyExpression); |  | 
| 211 |  | 
| 212         this.patterns.push({ |  | 
| 213           text: pattern.text, |  | 
| 214           regexp: new RegExp(regexpString, "i"), |  | 
| 215           prefix: pattern.selector.substr(0, match.index), |  | 
| 216           suffix: pattern.selector.substr(i + 1) |  | 
| 217         }); |  | 
| 218       } | 414       } | 
| 219 | 415 | 
| 220       if (this.patterns.length > 0) | 416       if (this.patterns.length > 0) | 
| 221       { | 417       { | 
| 222         let {document} = this.window; | 418         let {document} = this.window; | 
| 223         this.addSelectors(document.styleSheets); | 419         this.addSelectors(document.styleSheets); | 
| 224         document.addEventListener("load", this.onLoad.bind(this), true); | 420         document.addEventListener("load", this.onLoad.bind(this), true); | 
| 225       } | 421       } | 
| 226     }); | 422     }); | 
| 227   } | 423   } | 
| 228 }; | 424 }; | 
| OLD | NEW | 
|---|