| 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 | 
|---|