| OLD | NEW | 
|---|
|  | (Empty) | 
| 1 /* |  | 
| 2  * This file is part of Adblock Plus <https://adblockplus.org/>, |  | 
| 3  * Copyright (C) 2006-2016 Eyeo GmbH |  | 
| 4  * |  | 
| 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 |  | 
| 7  * published by the Free Software Foundation. |  | 
| 8  * |  | 
| 9  * Adblock Plus is distributed in the hope that it will be useful, |  | 
| 10  * but WITHOUT ANY WARRANTY; without even the implied warranty of |  | 
| 11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the |  | 
| 12  * GNU General Public License for more details. |  | 
| 13  * |  | 
| 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/>. |  | 
| 16  */ |  | 
| 17 |  | 
| 18 /** |  | 
| 19  * @fileOverview Element hiding implementation. |  | 
| 20  */ |  | 
| 21 |  | 
| 22 Cu.import("resource://gre/modules/Services.jsm"); |  | 
| 23 |  | 
| 24 var {Utils} = require("utils"); |  | 
| 25 var {IO} = require("io"); |  | 
| 26 var {Prefs} = require("prefs"); |  | 
| 27 var {ElemHideException} = require("filterClasses"); |  | 
| 28 var {FilterNotifier} = require("filterNotifier"); |  | 
| 29 |  | 
| 30 /** |  | 
| 31  * Lookup table, filters by their associated key |  | 
| 32  * @type Object |  | 
| 33  */ |  | 
| 34 var filterByKey = Object.create(null); |  | 
| 35 |  | 
| 36 /** |  | 
| 37  * Lookup table, keys of the filters by filter text |  | 
| 38  * @type Object |  | 
| 39  */ |  | 
| 40 var keyByFilter = Object.create(null); |  | 
| 41 |  | 
| 42 /** |  | 
| 43  * Lookup table, keys are known element hiding exceptions |  | 
| 44  * @type Object |  | 
| 45  */ |  | 
| 46 var knownExceptions = Object.create(null); |  | 
| 47 |  | 
| 48 /** |  | 
| 49  * Lookup table, lists of element hiding exceptions by selector |  | 
| 50  * @type Object |  | 
| 51  */ |  | 
| 52 var exceptions = Object.create(null); |  | 
| 53 |  | 
| 54 /** |  | 
| 55  * Currently applied stylesheet URL |  | 
| 56  * @type nsIURI |  | 
| 57  */ |  | 
| 58 var styleURL = null; |  | 
| 59 |  | 
| 60 /** |  | 
| 61  * Element hiding component |  | 
| 62  * @class |  | 
| 63  */ |  | 
| 64 var ElemHide = exports.ElemHide = |  | 
| 65 { |  | 
| 66   /** |  | 
| 67    * Indicates whether filters have been added or removed since the last apply()
      call. |  | 
| 68    * @type Boolean |  | 
| 69    */ |  | 
| 70   isDirty: false, |  | 
| 71 |  | 
| 72   /** |  | 
| 73    * Inidicates whether the element hiding stylesheet is currently applied. |  | 
| 74    * @type Boolean |  | 
| 75    */ |  | 
| 76   applied: false, |  | 
| 77 |  | 
| 78   /** |  | 
| 79    * Called on module startup. |  | 
| 80    */ |  | 
| 81   init: function() |  | 
| 82   { |  | 
| 83     Prefs.addListener(function(name) |  | 
| 84     { |  | 
| 85       if (name == "enabled") |  | 
| 86         ElemHide.apply(); |  | 
| 87     }); |  | 
| 88     onShutdown.add(() => ElemHide.unapply()); |  | 
| 89 |  | 
| 90     let styleFile = IO.resolveFilePath(Prefs.data_directory); |  | 
| 91     styleFile.append("elemhide.css"); |  | 
| 92     styleURL = Services.io.newFileURI(styleFile).QueryInterface(Ci.nsIFileURL); |  | 
| 93   }, |  | 
| 94 |  | 
| 95   /** |  | 
| 96    * Removes all known filters |  | 
| 97    */ |  | 
| 98   clear: function() |  | 
| 99   { |  | 
| 100     filterByKey = Object.create(null); |  | 
| 101     keyByFilter = Object.create(null); |  | 
| 102     knownExceptions = Object.create(null); |  | 
| 103     exceptions = Object.create(null); |  | 
| 104     ElemHide.isDirty = false; |  | 
| 105     ElemHide.unapply(); |  | 
| 106   }, |  | 
| 107 |  | 
| 108   /** |  | 
| 109    * Add a new element hiding filter |  | 
| 110    * @param {ElemHideFilter} filter |  | 
| 111    */ |  | 
| 112   add: function(filter) |  | 
| 113   { |  | 
| 114     if (filter instanceof ElemHideException) |  | 
| 115     { |  | 
| 116       if (filter.text in knownExceptions) |  | 
| 117         return; |  | 
| 118 |  | 
| 119       let selector = filter.selector; |  | 
| 120       if (!(selector in exceptions)) |  | 
| 121         exceptions[selector] = []; |  | 
| 122       exceptions[selector].push(filter); |  | 
| 123       knownExceptions[filter.text] = true; |  | 
| 124     } |  | 
| 125     else |  | 
| 126     { |  | 
| 127       if (filter.text in keyByFilter) |  | 
| 128         return; |  | 
| 129 |  | 
| 130       let key; |  | 
| 131       do { |  | 
| 132         key = Math.random().toFixed(15).substr(5); |  | 
| 133       } while (key in filterByKey); |  | 
| 134 |  | 
| 135       filterByKey[key] = filter; |  | 
| 136       keyByFilter[filter.text] = key; |  | 
| 137       ElemHide.isDirty = true; |  | 
| 138     } |  | 
| 139   }, |  | 
| 140 |  | 
| 141   /** |  | 
| 142    * Removes an element hiding filter |  | 
| 143    * @param {ElemHideFilter} filter |  | 
| 144    */ |  | 
| 145   remove: function(filter) |  | 
| 146   { |  | 
| 147     if (filter instanceof ElemHideException) |  | 
| 148     { |  | 
| 149       if (!(filter.text in knownExceptions)) |  | 
| 150         return; |  | 
| 151 |  | 
| 152       let list = exceptions[filter.selector]; |  | 
| 153       let index = list.indexOf(filter); |  | 
| 154       if (index >= 0) |  | 
| 155         list.splice(index, 1); |  | 
| 156       delete knownExceptions[filter.text]; |  | 
| 157     } |  | 
| 158     else |  | 
| 159     { |  | 
| 160       if (!(filter.text in keyByFilter)) |  | 
| 161         return; |  | 
| 162 |  | 
| 163       let key = keyByFilter[filter.text]; |  | 
| 164       delete filterByKey[key]; |  | 
| 165       delete keyByFilter[filter.text]; |  | 
| 166       ElemHide.isDirty = true; |  | 
| 167     } |  | 
| 168   }, |  | 
| 169 |  | 
| 170   /** |  | 
| 171    * Checks whether an exception rule is registered for a filter on a particular |  | 
| 172    * domain. |  | 
| 173    */ |  | 
| 174   getException: function(/**Filter*/ filter, /**String*/ docDomain) /**ElemHideE
     xception*/ |  | 
| 175   { |  | 
| 176     if (!(filter.selector in exceptions)) |  | 
| 177       return null; |  | 
| 178 |  | 
| 179     let list = exceptions[filter.selector]; |  | 
| 180     for (let i = list.length - 1; i >= 0; i--) |  | 
| 181       if (list[i].isActiveOnDomain(docDomain)) |  | 
| 182         return list[i]; |  | 
| 183 |  | 
| 184     return null; |  | 
| 185   }, |  | 
| 186 |  | 
| 187   /** |  | 
| 188    * Will be set to true if apply() is running (reentrance protection). |  | 
| 189    * @type Boolean |  | 
| 190    */ |  | 
| 191   _applying: false, |  | 
| 192 |  | 
| 193   /** |  | 
| 194    * Will be set to true if an apply() call arrives while apply() is already |  | 
| 195    * running (delayed execution). |  | 
| 196    * @type Boolean |  | 
| 197    */ |  | 
| 198   _needsApply: false, |  | 
| 199 |  | 
| 200   /** |  | 
| 201    * Generates stylesheet URL and applies it globally |  | 
| 202    */ |  | 
| 203   apply: function() |  | 
| 204   { |  | 
| 205     if (this._applying) |  | 
| 206     { |  | 
| 207       this._needsApply = true; |  | 
| 208       return; |  | 
| 209     } |  | 
| 210 |  | 
| 211     if (!ElemHide.isDirty || !Prefs.enabled) |  | 
| 212     { |  | 
| 213       // Nothing changed, looks like we merely got enabled/disabled |  | 
| 214       if (Prefs.enabled && !ElemHide.applied) |  | 
| 215       { |  | 
| 216         try |  | 
| 217         { |  | 
| 218           Utils.styleService.loadAndRegisterSheet(styleURL, Ci.nsIStyleSheetServ
     ice.USER_SHEET); |  | 
| 219           ElemHide.applied = true; |  | 
| 220         } |  | 
| 221         catch (e) |  | 
| 222         { |  | 
| 223           Cu.reportError(e); |  | 
| 224         } |  | 
| 225       } |  | 
| 226       else if (!Prefs.enabled && ElemHide.applied) |  | 
| 227       { |  | 
| 228         ElemHide.unapply(); |  | 
| 229       } |  | 
| 230 |  | 
| 231       return; |  | 
| 232     } |  | 
| 233 |  | 
| 234     IO.writeToFile(styleURL.file, this._generateCSSContent(), function(e) |  | 
| 235     { |  | 
| 236       this._applying = false; |  | 
| 237 |  | 
| 238       // _generateCSSContent is throwing NS_ERROR_NOT_AVAILABLE to indicate that |  | 
| 239       // there are no filters. If that exception is passed through XPCOM we will |  | 
| 240       // see a proper exception here, otherwise a number. |  | 
| 241       let noFilters = (e == Cr.NS_ERROR_NOT_AVAILABLE || (e && e.result == Cr.NS
     _ERROR_NOT_AVAILABLE)); |  | 
| 242       if (noFilters) |  | 
| 243       { |  | 
| 244         e = null; |  | 
| 245         IO.removeFile(styleURL.file, function(e) {}); |  | 
| 246       } |  | 
| 247       else if (e) |  | 
| 248         Cu.reportError(e); |  | 
| 249 |  | 
| 250       if (this._needsApply) |  | 
| 251       { |  | 
| 252         this._needsApply = false; |  | 
| 253         this.apply(); |  | 
| 254       } |  | 
| 255       else if (!e) |  | 
| 256       { |  | 
| 257         ElemHide.isDirty = false; |  | 
| 258 |  | 
| 259         ElemHide.unapply(); |  | 
| 260 |  | 
| 261         if (!noFilters) |  | 
| 262         { |  | 
| 263           try |  | 
| 264           { |  | 
| 265             Utils.styleService.loadAndRegisterSheet(styleURL, Ci.nsIStyleSheetSe
     rvice.USER_SHEET); |  | 
| 266             ElemHide.applied = true; |  | 
| 267           } |  | 
| 268           catch (e) |  | 
| 269           { |  | 
| 270             Cu.reportError(e); |  | 
| 271           } |  | 
| 272         } |  | 
| 273 |  | 
| 274         FilterNotifier.triggerListeners("elemhideupdate"); |  | 
| 275       } |  | 
| 276     }.bind(this)); |  | 
| 277 |  | 
| 278     this._applying = true; |  | 
| 279   }, |  | 
| 280 |  | 
| 281   _generateCSSContent: function*() |  | 
| 282   { |  | 
| 283     // Grouping selectors by domains |  | 
| 284     let domains = Object.create(null); |  | 
| 285     let hasFilters = false; |  | 
| 286     for (let key in filterByKey) |  | 
| 287     { |  | 
| 288       let filter = filterByKey[key]; |  | 
| 289       let domain = filter.selectorDomain || ""; |  | 
| 290 |  | 
| 291       let list; |  | 
| 292       if (domain in domains) |  | 
| 293         list = domains[domain]; |  | 
| 294       else |  | 
| 295       { |  | 
| 296         list = Object.create(null); |  | 
| 297         domains[domain] = list; |  | 
| 298       } |  | 
| 299       list[filter.selector] = key; |  | 
| 300       hasFilters = true; |  | 
| 301     } |  | 
| 302 |  | 
| 303     if (!hasFilters) |  | 
| 304       throw Cr.NS_ERROR_NOT_AVAILABLE; |  | 
| 305 |  | 
| 306     function escapeChar(match) |  | 
| 307     { |  | 
| 308       return "\\" + match.charCodeAt(0).toString(16) + " "; |  | 
| 309     } |  | 
| 310 |  | 
| 311     // Return CSS data |  | 
| 312     let cssTemplate = "-moz-binding: url(about:abp-elemhidehit?%ID%#dummy) !impo
     rtant;"; |  | 
| 313     for (let domain in domains) |  | 
| 314     { |  | 
| 315       let rules = []; |  | 
| 316       let list = domains[domain]; |  | 
| 317 |  | 
| 318       if (domain) |  | 
| 319         yield ('@-moz-document domain("' + domain.split(",").join('"),domain("')
      + '"){').replace(/[^\x01-\x7F]/g, escapeChar); |  | 
| 320       else |  | 
| 321       { |  | 
| 322         // Only allow unqualified rules on a few protocols to prevent them from 
     blocking chrome |  | 
| 323         yield '@-moz-document url-prefix("http://"),url-prefix("https://"),' |  | 
| 324                   + 'url-prefix("mailbox://"),url-prefix("imap://"),' |  | 
| 325                   + 'url-prefix("news://"),url-prefix("snews://"){'; |  | 
| 326       } |  | 
| 327 |  | 
| 328       for (let selector in list) |  | 
| 329         yield selector.replace(/[^\x01-\x7F]/g, escapeChar) + "{" + cssTemplate.
     replace("%ID%", list[selector]) + "}"; |  | 
| 330       yield '}'; |  | 
| 331     } |  | 
| 332   }, |  | 
| 333 |  | 
| 334   /** |  | 
| 335    * Unapplies current stylesheet URL |  | 
| 336    */ |  | 
| 337   unapply: function() |  | 
| 338   { |  | 
| 339     if (ElemHide.applied) |  | 
| 340     { |  | 
| 341       try |  | 
| 342       { |  | 
| 343         Utils.styleService.unregisterSheet(styleURL, Ci.nsIStyleSheetService.USE
     R_SHEET); |  | 
| 344       } |  | 
| 345       catch (e) |  | 
| 346       { |  | 
| 347         Cu.reportError(e); |  | 
| 348       } |  | 
| 349       ElemHide.applied = false; |  | 
| 350     } |  | 
| 351   }, |  | 
| 352 |  | 
| 353   /** |  | 
| 354    * Retrieves the currently applied stylesheet URL |  | 
| 355    * @type String |  | 
| 356    */ |  | 
| 357   get styleURL() |  | 
| 358   { |  | 
| 359     return ElemHide.applied ? styleURL.spec : null; |  | 
| 360   }, |  | 
| 361 |  | 
| 362   /** |  | 
| 363    * Retrieves an element hiding filter by the corresponding protocol key |  | 
| 364    */ |  | 
| 365   getFilterByKey: function(/**String*/ key) /**Filter*/ |  | 
| 366   { |  | 
| 367     return (key in filterByKey ? filterByKey[key] : null); |  | 
| 368   }, |  | 
| 369 |  | 
| 370   /** |  | 
| 371    * Returns a list of all selectors active on a particular domain (currently |  | 
| 372    * used only in Chrome, Opera and Safari). |  | 
| 373    */ |  | 
| 374   getSelectorsForDomain: function(/**String*/ domain, /**Boolean*/ specificOnly) |  | 
| 375   { |  | 
| 376     let result = []; |  | 
| 377     let keys = Object.getOwnPropertyNames(filterByKey); |  | 
| 378     for (let key of keys) |  | 
| 379     { |  | 
| 380       let filter = filterByKey[key]; |  | 
| 381       if (specificOnly && (!filter.domains || filter.domains[""])) |  | 
| 382         continue; |  | 
| 383 |  | 
| 384       if (filter.isActiveOnDomain(domain) && !this.getException(filter, domain)) |  | 
| 385         result.push(filter.selector); |  | 
| 386     } |  | 
| 387     return result; |  | 
| 388   } |  | 
| 389 }; |  | 
| OLD | NEW | 
|---|