| 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\((.*)\)/; | 
| 5 | 6 | 
| 6 function splitSelector(selector) | 7 function splitSelector(selector) | 
| 7 { | 8 { | 
| 8   if (selector.indexOf(",") == -1) | 9   if (selector.indexOf(",") == -1) | 
| 9     return [selector]; | 10     return [selector]; | 
| 10 | 11 | 
| 11   var selectors = []; | 12   var selectors = []; | 
| 12   var start = 0; | 13   var start = 0; | 
| 13   var level = 0; | 14   var level = 0; | 
| 14   var sep = ""; | 15   var sep = ""; | 
| (...skipping 19 matching lines...) Expand all  Loading... | 
| 34         selectors.push(selector.substring(start, i)); | 35         selectors.push(selector.substring(start, i)); | 
| 35         start = i + 1; | 36         start = i + 1; | 
| 36       } | 37       } | 
| 37     } | 38     } | 
| 38   } | 39   } | 
| 39 | 40 | 
| 40   selectors.push(selector.substring(start)); | 41   selectors.push(selector.substring(start)); | 
| 41   return selectors; | 42   return selectors; | 
| 42 } | 43 } | 
| 43 | 44 | 
| 44 function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc) | 45 const ABP_ATTR = "abp-0ac791f0-a03b-4f2c-a935-a285bc2e668e"; | 
|  | 46 const ABP_ATTR_SEL = "[" + ABP_ATTR + "] "; | 
|  | 47 | 
|  | 48 function matchChildren(e, selector) | 
|  | 49 { | 
|  | 50   var subElement; | 
|  | 51   var newSelector = ABP_ATTR_SEL + selector; | 
|  | 52   var parentNode = e.parentNode || document; | 
|  | 53 | 
|  | 54   e.setAttribute(ABP_ATTR, ""); | 
|  | 55   subElement = parentNode.querySelector(newSelector); | 
|  | 56   e.removeAttribute(ABP_ATTR); | 
|  | 57 | 
|  | 58   return subElement != null; | 
|  | 59 } | 
|  | 60 | 
|  | 61 function selectChildren(e, selector) | 
|  | 62 { | 
|  | 63   var subElements; | 
|  | 64   var newSelector = ABP_ATTR_SEL + selector; | 
|  | 65   var parentNode = e.parentNode || document; | 
|  | 66 | 
|  | 67   e.setAttribute(ABP_ATTR, ""); | 
|  | 68   subElements = parentNode.querySelectorAll(newSelector); | 
|  | 69   e.removeAttribute(ABP_ATTR); | 
|  | 70 | 
|  | 71   return subElements; | 
|  | 72 } | 
|  | 73 | 
|  | 74 function parsePattern(pattern) | 
|  | 75 { | 
|  | 76   // we should catch the :has() pseudo class first. | 
|  | 77   var match = pseudoClassHasSelectorRegExp.exec(pattern.selector); | 
|  | 78   if (match) | 
|  | 79   { | 
|  | 80     return { | 
|  | 81       type: "has", | 
|  | 82       text: pattern.text, | 
|  | 83       elementMatcher: new PseudoHasMatcher(match[1]), | 
|  | 84       prefix: pattern.selector.substr(0, match.index).trim(), | 
|  | 85       suffix: pattern.selector.substr(match.index + match[0].length).trim() | 
|  | 86     }; | 
|  | 87   } | 
|  | 88 | 
|  | 89   match = propertySelectorRegExp.exec(pattern.selector); | 
|  | 90   if (match) | 
|  | 91   { | 
|  | 92     var regexpString; | 
|  | 93     var propertyExpression = match[2]; | 
|  | 94     if (propertyExpression.length >= 2 && propertyExpression[0] == "/" && | 
|  | 95         propertyExpression[propertyExpression.length - 1] == "/") | 
|  | 96       regexpString = propertyExpression.slice(1, -1) | 
|  | 97       .replace("\\x7B ", "{").replace("\\x7D ", "}"); | 
|  | 98     else | 
|  | 99       regexpString = filterToRegExp(propertyExpression); | 
|  | 100     return { | 
|  | 101       type: "props", | 
|  | 102       text: pattern.text, | 
|  | 103       regexp: new RegExp(regexpString, "i"), | 
|  | 104       prefix: pattern.selector.substr(0, match.index), | 
|  | 105       suffix: pattern.selector.substr(match.index + match[0].length) | 
|  | 106     }; | 
|  | 107   } | 
|  | 108 } | 
|  | 109 | 
|  | 110 function matchStyleProps(style, rule, pattern, selectors, filters) | 
|  | 111 { | 
|  | 112   if (pattern.regexp.test(style)) | 
|  | 113   { | 
|  | 114     var subSelectors = splitSelector(rule.selectorText); | 
|  | 115     for (var i = 0; i < subSelectors.length; i++) | 
|  | 116     { | 
|  | 117       var subSelector = subSelectors[i]; | 
|  | 118       selectors.push(pattern.prefix + subSelector + pattern.suffix); | 
|  | 119       filters.push(pattern.text); | 
|  | 120     } | 
|  | 121   } | 
|  | 122 } | 
|  | 123 | 
|  | 124 function findPropsSelectors(stylesheet, patterns, selectors, filters) | 
|  | 125 { | 
|  | 126   var rules = stylesheet.cssRules; | 
|  | 127   if (!rules) | 
|  | 128     return; | 
|  | 129 | 
|  | 130   for (var i = 0; i < rules.length; i++) | 
|  | 131   { | 
|  | 132     var rule = rules[i]; | 
|  | 133     if (rule.type != rule.STYLE_RULE) | 
|  | 134       continue; | 
|  | 135 | 
|  | 136     var style = stringifyStyle(rule.style); | 
|  | 137     for (var j = 0; j < patterns.length; j++) | 
|  | 138     { | 
|  | 139       matchStyleProps(style, rule, patterns[j], selectors, filters); | 
|  | 140     } | 
|  | 141   } | 
|  | 142 } | 
|  | 143 | 
|  | 144 /** | 
|  | 145  * Match the selector @pattern containing :has() starting from @node. | 
|  | 146  */ | 
|  | 147 function pseudoClassHasMatch(pattern, node, stylesheets, elements, filters) | 
|  | 148 { | 
|  | 149   // select element for the prefix pattern. Or just use node. | 
|  | 150   var haveEl = pattern.prefix ? node.querySelectorAll(pattern.prefix) : [ node ]
     ; | 
|  | 151   for (var j = 0; j < haveEl.length; j++) | 
|  | 152   { | 
|  | 153     var matched = pattern.elementMatcher.match(haveEl[j], stylesheets); | 
|  | 154     if (!matched) | 
|  | 155       continue; | 
|  | 156 | 
|  | 157     if (pattern.suffix) | 
|  | 158     { | 
|  | 159       var subElements = selectChildren(haveEl[j], pattern.suffix); | 
|  | 160       if (subElements) | 
|  | 161       { | 
|  | 162         for (var k = 0; k < subElements.length; k++) | 
|  | 163         { | 
|  | 164           elements.push(subElements[k]); | 
|  | 165           filters.push(pattern.text); | 
|  | 166         } | 
|  | 167       } | 
|  | 168     } | 
|  | 169     else | 
|  | 170     { | 
|  | 171       elements.push(haveEl[j]); | 
|  | 172       filters.push(pattern.text); | 
|  | 173     } | 
|  | 174   } | 
|  | 175 } | 
|  | 176 | 
|  | 177 function stringifyStyle(style) | 
|  | 178 { | 
|  | 179   var styles = []; | 
|  | 180   for (var i = 0; i < style.length; i++) | 
|  | 181   { | 
|  | 182     var property = style.item(i); | 
|  | 183     var value    = style.getPropertyValue(property); | 
|  | 184     var priority = style.getPropertyPriority(property); | 
|  | 185     styles.push(property + ": " + value + (priority ? " !" + priority : "") + ";
     "); | 
|  | 186   } | 
|  | 187   styles.sort(); | 
|  | 188   return styles.join(" "); | 
|  | 189 } | 
|  | 190 | 
|  | 191 // matcher for the pseudo CSS4 class :has | 
|  | 192 // For those browser that don't have it yet. | 
|  | 193 function PseudoHasMatcher(selector) | 
|  | 194 { | 
|  | 195   this.hasSelector = selector; | 
|  | 196   this.parsed = parsePattern({ selector: this.hasSelector }); | 
|  | 197 } | 
|  | 198 | 
|  | 199 PseudoHasMatcher.prototype = { | 
|  | 200   match: function(elem, stylesheets) | 
|  | 201   { | 
|  | 202     var selectors = []; | 
|  | 203 | 
|  | 204     if (this.parsed) | 
|  | 205     { | 
|  | 206       var filters = []; // don't need this, we have a partial filter. | 
|  | 207       if (this.parsed.type == "has") | 
|  | 208       { | 
|  | 209         var child = elem.firstChild; | 
|  | 210         while (child) | 
|  | 211         { | 
|  | 212           if (child.nodeType === Node.ELEMENT_NODE) | 
|  | 213           { | 
|  | 214             var matches = []; | 
|  | 215             pseudoClassHasMatch(this.parsed, child, stylesheets, matches, filter
     s) | 
|  | 216             if (matches.length > 0) | 
|  | 217               return true; | 
|  | 218           } | 
|  | 219           child = child.nextSibling; | 
|  | 220         } | 
|  | 221         return false; | 
|  | 222       } | 
|  | 223       if (this.parsed.type == "props") | 
|  | 224       { | 
|  | 225         for (var i = 0; i  < stylesheets.length; i++) | 
|  | 226           findPropsSelectors(stylesheets[i], [this.parsed], selectors, filters); | 
|  | 227       } | 
|  | 228     } | 
|  | 229     else | 
|  | 230     { | 
|  | 231       selectors = [this.hasSelector]; | 
|  | 232     } | 
|  | 233 | 
|  | 234     var matched = false; | 
|  | 235     // look up for all elements that match the :has(). | 
|  | 236     for (var k = 0; k < selectors.length; k++) | 
|  | 237     { | 
|  | 238       try | 
|  | 239       { | 
|  | 240         matched = matchChildren(elem, selectors[k]); | 
|  | 241         if (matched) | 
|  | 242           break; | 
|  | 243       } | 
|  | 244       catch(e) | 
|  | 245       { | 
|  | 246         console.error("Exception with querySelector()", selectors[k], e); | 
|  | 247       } | 
|  | 248     } | 
|  | 249     return matched; | 
|  | 250   } | 
|  | 251 }; | 
|  | 252 | 
|  | 253 function ElemHideEmulation(window, getFiltersFunc, addSelectorsFunc, hideElement
     sFunc) | 
| 45 { | 254 { | 
| 46   this.window = window; | 255   this.window = window; | 
| 47   this.getFiltersFunc = getFiltersFunc; | 256   this.getFiltersFunc = getFiltersFunc; | 
| 48   this.addSelectorsFunc = addSelectorsFunc; | 257   this.addSelectorsFunc = addSelectorsFunc; | 
|  | 258   this.hideElementsFunc = hideElementsFunc; | 
| 49 } | 259 } | 
| 50 | 260 | 
| 51 ElemHideEmulation.prototype = { | 261 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 | 262 | 
| 66   isSameOrigin: function(stylesheet) | 263   isSameOrigin: function(stylesheet) | 
| 67   { | 264   { | 
| 68     try | 265     try | 
| 69     { | 266     { | 
| 70       return new URL(stylesheet.href).origin == this.window.location.origin; | 267       return new URL(stylesheet.href).origin == this.window.location.origin; | 
| 71     } | 268     } | 
| 72     catch (e) | 269     catch (e) | 
| 73     { | 270     { | 
| 74       // Invalid URL, assume that it is first-party. | 271       // Invalid URL, assume that it is first-party. | 
| 75       return true; | 272       return true; | 
| 76     } | 273     } | 
| 77   }, | 274   }, | 
| 78 | 275 | 
| 79   findSelectors: function(stylesheet, selectors, filters) | 276   findPseudoClassHasElements: function(node, stylesheets, elements, filters) | 
| 80   { | 277   { | 
| 81     // Explicitly ignore third-party stylesheets to ensure consistent behavior | 278     for (var i = 0; i < this.pseudoHasPatterns.length; i++) | 
| 82     // between Firefox and Chrome. | 279     { | 
| 83     if (!this.isSameOrigin(stylesheet)) | 280       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     } | 281     } | 
| 112   }, | 282   }, | 
| 113 | 283 | 
| 114   addSelectors: function(stylesheets) | 284   addSelectors: function(stylesheets) | 
| 115   { | 285   { | 
| 116     var selectors = []; | 286     var selectors = []; | 
| 117     var filters = []; | 287     var filters = []; | 
| 118     for (var i = 0; i < stylesheets.length; i++) | 288     for (var i = 0; i < stylesheets.length; i++) | 
| 119       this.findSelectors(stylesheets[i], selectors, filters); | 289     { | 
|  | 290       // Explicitly ignore third-party stylesheets to ensure consistent behavior | 
|  | 291       // between Firefox and Chrome. | 
|  | 292       if (!this.isSameOrigin(stylesheets[i])) | 
|  | 293         continue; | 
|  | 294       findPropsSelectors(stylesheets[i], this.propSelPatterns, selectors, filter
     s); | 
|  | 295     } | 
| 120     this.addSelectorsFunc(selectors, filters); | 296     this.addSelectorsFunc(selectors, filters); | 
| 121   }, | 297   }, | 
| 122 | 298 | 
|  | 299   hideElements: function(stylesheets) | 
|  | 300   { | 
|  | 301     var elements = []; | 
|  | 302     var filters = []; | 
|  | 303     this.findPseudoClassHasElements(document, stylesheets, elements, filters); | 
|  | 304     this.hideElementsFunc(elements, filters); | 
|  | 305   }, | 
|  | 306 | 
| 123   onLoad: function(event) | 307   onLoad: function(event) | 
| 124   { | 308   { | 
| 125     var stylesheet = event.target.sheet; | 309     var stylesheet = event.target.sheet; | 
| 126     if (stylesheet) | 310     if (stylesheet) | 
| 127       this.addSelectors([stylesheet]); | 311       this.addSelectors([stylesheet]); | 
|  | 312     this.hideElements([stylesheet]); | 
| 128   }, | 313   }, | 
| 129 | 314 | 
| 130   apply: function() | 315   apply: function() | 
| 131   { | 316   { | 
| 132     this.getFiltersFunc(function(patterns) | 317     this.getFiltersFunc(function(patterns) | 
| 133     { | 318     { | 
| 134       this.patterns = []; | 319       this.propSelPatterns = []; | 
|  | 320       this.pseudoHasPatterns = []; | 
| 135       for (var i = 0; i < patterns.length; i++) | 321       for (var i = 0; i < patterns.length; i++) | 
| 136       { | 322       { | 
| 137         var pattern = patterns[i]; | 323         var pattern = patterns[i]; | 
| 138         var match = propertySelectorRegExp.exec(pattern.selector); | 324         var parsed = parsePattern(pattern); | 
| 139         if (!match) | 325         if (parsed == undefined) | 
| 140           continue; | 326           continue; | 
| 141 | 327         if (parsed.type == "props") | 
| 142         var propertyExpression = match[2]; | 328         { | 
| 143         var regexpString; | 329           this.propSelPatterns.push(parsed); | 
| 144         if (propertyExpression.length >= 2 && propertyExpression[0] == "/" && | 330         } | 
| 145             propertyExpression[propertyExpression.length - 1] == "/") | 331         else if (parsed.type == "has") | 
| 146           regexpString = propertyExpression.slice(1, -1) | 332         { | 
| 147               .replace("\\x7B ", "{").replace("\\x7D ", "}"); | 333           this.pseudoHasPatterns.push(parsed); | 
| 148         else | 334         } | 
| 149           regexpString = filterToRegExp(propertyExpression); | 335       } | 
| 150 | 336 | 
| 151         this.patterns.push({ | 337       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       { | 338       { | 
| 161         var document = this.window.document; | 339         var document = this.window.document; | 
| 162         this.addSelectors(document.styleSheets); | 340         this.addSelectors(document.styleSheets); | 
|  | 341         this.hideElements(document.styleSheets); | 
| 163         document.addEventListener("load", this.onLoad.bind(this), true); | 342         document.addEventListener("load", this.onLoad.bind(this), true); | 
| 164       } | 343       } | 
| 165     }.bind(this)); | 344     }.bind(this)); | 
| 166   } | 345   } | 
| 167 }; | 346 }; | 
| OLD | NEW | 
|---|