| 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-2016 Eyeo GmbH | 3 * Copyright (C) 2006-2016 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 /** | 18 /** |
| 19 * @fileOverview Element hiding implementation. | 19 * @fileOverview Element hiding implementation. |
| 20 */ | 20 */ |
| 21 | 21 |
| 22 Cu.import("resource://gre/modules/Services.jsm"); | 22 let {Utils} = require("utils"); |
| 23 | 23 let {ElemHideException} = require("filterClasses"); |
| 24 var {Utils} = require("utils"); | 24 let {FilterNotifier} = require("filterNotifier"); |
| 25 var {IO} = require("io"); | |
| 26 var {Prefs} = require("prefs"); | |
| 27 var {ElemHideException} = require("filterClasses"); | |
| 28 var {FilterNotifier} = require("filterNotifier"); | |
| 29 | 25 |
| 30 /** | 26 /** |
| 31 * Lookup table, filters by their associated key | 27 * Lookup table, filters by their associated key |
| 32 * @type Object | 28 * @type Object |
| 33 */ | 29 */ |
| 34 var filterByKey = []; | 30 var filterByKey = []; |
| 35 | 31 |
| 36 /** | 32 /** |
| 37 * Lookup table, keys of the filters by filter text | 33 * Lookup table, keys of the filters by filter text |
| 38 * @type Object | 34 * @type Object |
| (...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 79 */ | 75 */ |
| 80 var knownExceptions = Object.create(null); | 76 var knownExceptions = Object.create(null); |
| 81 | 77 |
| 82 /** | 78 /** |
| 83 * Lookup table, lists of element hiding exceptions by selector | 79 * Lookup table, lists of element hiding exceptions by selector |
| 84 * @type Object | 80 * @type Object |
| 85 */ | 81 */ |
| 86 var exceptions = Object.create(null); | 82 var exceptions = Object.create(null); |
| 87 | 83 |
| 88 /** | 84 /** |
| 89 * Currently applied stylesheet URL | 85 * Container for element hiding filters |
| 90 * @type nsIURI | |
| 91 */ | |
| 92 var styleURL = null; | |
| 93 | |
| 94 /** | |
| 95 * Element hiding component | |
| 96 * @class | 86 * @class |
| 97 */ | 87 */ |
| 98 var ElemHide = exports.ElemHide = | 88 var ElemHide = exports.ElemHide = |
| 99 { | 89 { |
| 100 /** | 90 /** |
| 101 * Indicates whether filters have been added or removed since the last apply()
call. | |
| 102 * @type Boolean | |
| 103 */ | |
| 104 isDirty: false, | |
| 105 | |
| 106 /** | |
| 107 * Indicates whether the element hiding stylesheet is currently applied. | |
| 108 * @type Boolean | |
| 109 */ | |
| 110 applied: false, | |
| 111 | |
| 112 /** | |
| 113 * Called on module startup. | |
| 114 */ | |
| 115 init: function() | |
| 116 { | |
| 117 Prefs.addListener(function(name) | |
| 118 { | |
| 119 if (name == "enabled") | |
| 120 ElemHide.apply(); | |
| 121 }); | |
| 122 onShutdown.add(() => ElemHide.unapply()); | |
| 123 | |
| 124 let styleFile = IO.resolveFilePath(Prefs.data_directory); | |
| 125 styleFile.append("elemhide.css"); | |
| 126 styleURL = Services.io.newFileURI(styleFile).QueryInterface(Ci.nsIFileURL); | |
| 127 }, | |
| 128 | |
| 129 /** | |
| 130 * Removes all known filters | 91 * Removes all known filters |
| 131 */ | 92 */ |
| 132 clear: function() | 93 clear: function() |
| 133 { | 94 { |
| 134 filterByKey = []; | 95 filterByKey = []; |
| 135 keyByFilter = Object.create(null); | 96 keyByFilter = Object.create(null); |
| 136 filtersByDomain = Object.create(null); | 97 filtersByDomain = Object.create(null); |
| 137 filtersBySelector = Object.create(null); | 98 filtersBySelector = Object.create(null); |
| 138 unconditionalSelectors = null; | 99 unconditionalSelectors = null; |
| 139 knownExceptions = Object.create(null); | 100 knownExceptions = Object.create(null); |
| 140 exceptions = Object.create(null); | 101 exceptions = Object.create(null); |
| 141 ElemHide.isDirty = false; | 102 FilterNotifier.emit("elemhideupdate"); |
| 142 ElemHide.unapply(); | |
| 143 }, | 103 }, |
| 144 | 104 |
| 145 _addToFiltersByDomain: function(filter) | 105 _addToFiltersByDomain: function(filter) |
| 146 { | 106 { |
| 147 let key = keyByFilter[filter.text]; | 107 let key = keyByFilter[filter.text]; |
| 148 let domains = filter.domains || defaultDomains; | 108 let domains = filter.domains || defaultDomains; |
| 149 for (let domain in domains) | 109 for (let domain in domains) |
| 150 { | 110 { |
| 151 let filters = filtersByDomain[domain]; | 111 let filters = filtersByDomain[domain]; |
| 152 if (!filters) | 112 if (!filters) |
| (...skipping 62 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 215 filtersBySelector[filter.selector] = [filter]; | 175 filtersBySelector[filter.selector] = [filter]; |
| 216 unconditionalSelectors = null; | 176 unconditionalSelectors = null; |
| 217 } | 177 } |
| 218 } | 178 } |
| 219 else | 179 else |
| 220 { | 180 { |
| 221 // The new filter's selector only applies to some domains | 181 // The new filter's selector only applies to some domains |
| 222 this._addToFiltersByDomain(filter); | 182 this._addToFiltersByDomain(filter); |
| 223 } | 183 } |
| 224 } | 184 } |
| 185 } |
| 225 | 186 |
| 226 ElemHide.isDirty = true; | 187 FilterNotifier.emit("elemhideupdate"); |
| 227 } | |
| 228 }, | 188 }, |
| 229 | 189 |
| 230 /** | 190 /** |
| 231 * Removes an element hiding filter | 191 * Removes an element hiding filter |
| 232 * @param {ElemHideFilter} filter | 192 * @param {ElemHideFilter} filter |
| 233 */ | 193 */ |
| 234 remove: function(filter) | 194 remove: function(filter) |
| 235 { | 195 { |
| 236 if (filter instanceof ElemHideException) | 196 if (filter instanceof ElemHideException) |
| 237 { | 197 { |
| 238 if (!(filter.text in knownExceptions)) | 198 if (!(filter.text in knownExceptions)) |
| 239 return; | 199 return; |
| 240 | 200 |
| 241 let list = exceptions[filter.selector]; | 201 let list = exceptions[filter.selector]; |
| 242 let index = list.indexOf(filter); | 202 let index = list.indexOf(filter); |
| 243 if (index >= 0) | 203 if (index >= 0) |
| 244 list.splice(index, 1); | 204 list.splice(index, 1); |
| 245 delete knownExceptions[filter.text]; | 205 delete knownExceptions[filter.text]; |
| 246 } | 206 } |
| 247 else | 207 else |
| 248 { | 208 { |
| 249 if (!(filter.text in keyByFilter)) | 209 if (!(filter.text in keyByFilter)) |
| 250 return; | 210 return; |
| 251 | 211 |
| 252 let key = keyByFilter[filter.text]; | 212 let key = keyByFilter[filter.text]; |
| 253 delete filterByKey[key]; | 213 delete filterByKey[key]; |
| 254 delete keyByFilter[filter.text]; | 214 delete keyByFilter[filter.text]; |
| 255 ElemHide.isDirty = true; | |
| 256 | 215 |
| 257 if (usingGetSelectorsForDomain) | 216 if (usingGetSelectorsForDomain) |
| 258 { | 217 { |
| 259 let filters = filtersBySelector[filter.selector]; | 218 let filters = filtersBySelector[filter.selector]; |
| 260 if (filters) | 219 if (filters) |
| 261 { | 220 { |
| 262 if (filters.length > 1) | 221 if (filters.length > 1) |
| 263 { | 222 { |
| 264 let index = filters.indexOf(filter); | 223 let index = filters.indexOf(filter); |
| 265 filters.splice(index, 1); | 224 filters.splice(index, 1); |
| 266 } | 225 } |
| 267 else | 226 else |
| 268 { | 227 { |
| 269 delete filtersBySelector[filter.selector]; | 228 delete filtersBySelector[filter.selector]; |
| 270 unconditionalSelectors = null; | 229 unconditionalSelectors = null; |
| 271 } | 230 } |
| 272 } | 231 } |
| 273 else | 232 else |
| 274 { | 233 { |
| 275 let domains = filter.domains || defaultDomains; | 234 let domains = filter.domains || defaultDomains; |
| 276 for (let domain in domains) | 235 for (let domain in domains) |
| 277 { | 236 { |
| 278 let filters = filtersByDomain[domain]; | 237 let filters = filtersByDomain[domain]; |
| 279 if (filters) | 238 if (filters) |
| 280 delete filters[key]; | 239 delete filters[key]; |
| 281 } | 240 } |
| 282 } | 241 } |
| 283 } | 242 } |
| 284 } | 243 } |
| 244 |
| 245 FilterNotifier.emit("elemhideupdate"); |
| 285 }, | 246 }, |
| 286 | 247 |
| 287 /** | 248 /** |
| 288 * Checks whether an exception rule is registered for a filter on a particular | 249 * Checks whether an exception rule is registered for a filter on a particular |
| 289 * domain. | 250 * domain. |
| 290 */ | 251 */ |
| 291 getException: function(/**Filter*/ filter, /**String*/ docDomain) /**ElemHideE
xception*/ | 252 getException: function(/**Filter*/ filter, /**String*/ docDomain) /**ElemHideE
xception*/ |
| 292 { | 253 { |
| 293 if (!(filter.selector in exceptions)) | 254 if (!(filter.selector in exceptions)) |
| 294 return null; | 255 return null; |
| 295 | 256 |
| 296 let list = exceptions[filter.selector]; | 257 let list = exceptions[filter.selector]; |
| 297 for (let i = list.length - 1; i >= 0; i--) | 258 for (let i = list.length - 1; i >= 0; i--) |
| 298 if (list[i].isActiveOnDomain(docDomain)) | 259 if (list[i].isActiveOnDomain(docDomain)) |
| 299 return list[i]; | 260 return list[i]; |
| 300 | 261 |
| 301 return null; | 262 return null; |
| 302 }, | 263 }, |
| 303 | 264 |
| 304 /** | 265 /** |
| 305 * Will be set to true if apply() is running (reentrance protection). | |
| 306 * @type Boolean | |
| 307 */ | |
| 308 _applying: false, | |
| 309 | |
| 310 /** | |
| 311 * Will be set to true if an apply() call arrives while apply() is already | |
| 312 * running (delayed execution). | |
| 313 * @type Boolean | |
| 314 */ | |
| 315 _needsApply: false, | |
| 316 | |
| 317 /** | |
| 318 * Generates stylesheet URL and applies it globally | |
| 319 */ | |
| 320 apply: function() | |
| 321 { | |
| 322 if (this._applying) | |
| 323 { | |
| 324 this._needsApply = true; | |
| 325 return; | |
| 326 } | |
| 327 | |
| 328 if (!ElemHide.isDirty || !Prefs.enabled) | |
| 329 { | |
| 330 // Nothing changed, looks like we merely got enabled/disabled | |
| 331 if (Prefs.enabled && !ElemHide.applied) | |
| 332 { | |
| 333 try | |
| 334 { | |
| 335 Utils.styleService.loadAndRegisterSheet(styleURL, Ci.nsIStyleSheetServ
ice.USER_SHEET); | |
| 336 ElemHide.applied = true; | |
| 337 } | |
| 338 catch (e) | |
| 339 { | |
| 340 Cu.reportError(e); | |
| 341 } | |
| 342 } | |
| 343 else if (!Prefs.enabled && ElemHide.applied) | |
| 344 { | |
| 345 ElemHide.unapply(); | |
| 346 } | |
| 347 | |
| 348 return; | |
| 349 } | |
| 350 | |
| 351 IO.writeToFile(styleURL.file, this._generateCSSContent(), function(e) | |
| 352 { | |
| 353 this._applying = false; | |
| 354 | |
| 355 // _generateCSSContent is throwing NS_ERROR_NOT_AVAILABLE to indicate that | |
| 356 // there are no filters. If that exception is passed through XPCOM we will | |
| 357 // see a proper exception here, otherwise a number. | |
| 358 let noFilters = (e == Cr.NS_ERROR_NOT_AVAILABLE || (e && e.result == Cr.NS
_ERROR_NOT_AVAILABLE)); | |
| 359 if (noFilters) | |
| 360 { | |
| 361 e = null; | |
| 362 IO.removeFile(styleURL.file, function(e) {}); | |
| 363 } | |
| 364 else if (e) | |
| 365 Cu.reportError(e); | |
| 366 | |
| 367 if (this._needsApply) | |
| 368 { | |
| 369 this._needsApply = false; | |
| 370 this.apply(); | |
| 371 } | |
| 372 else if (!e) | |
| 373 { | |
| 374 ElemHide.isDirty = false; | |
| 375 | |
| 376 ElemHide.unapply(); | |
| 377 | |
| 378 if (!noFilters) | |
| 379 { | |
| 380 try | |
| 381 { | |
| 382 Utils.styleService.loadAndRegisterSheet(styleURL, Ci.nsIStyleSheetSe
rvice.USER_SHEET); | |
| 383 ElemHide.applied = true; | |
| 384 } | |
| 385 catch (e) | |
| 386 { | |
| 387 Cu.reportError(e); | |
| 388 } | |
| 389 } | |
| 390 | |
| 391 FilterNotifier.triggerListeners("elemhideupdate"); | |
| 392 } | |
| 393 }.bind(this)); | |
| 394 | |
| 395 this._applying = true; | |
| 396 }, | |
| 397 | |
| 398 _generateCSSContent: function*() | |
| 399 { | |
| 400 // Grouping selectors by domains | |
| 401 let domains = Object.create(null); | |
| 402 let hasFilters = false; | |
| 403 for (let key in filterByKey) | |
| 404 { | |
| 405 let filter = filterByKey[key]; | |
| 406 let selector = filter.selector; | |
| 407 if (!selector) | |
| 408 continue; | |
| 409 | |
| 410 let domain = filter.selectorDomain || ""; | |
| 411 | |
| 412 let list; | |
| 413 if (domain in domains) | |
| 414 list = domains[domain]; | |
| 415 else | |
| 416 { | |
| 417 list = Object.create(null); | |
| 418 domains[domain] = list; | |
| 419 } | |
| 420 list[selector] = key; | |
| 421 hasFilters = true; | |
| 422 } | |
| 423 | |
| 424 if (!hasFilters) | |
| 425 throw Cr.NS_ERROR_NOT_AVAILABLE; | |
| 426 | |
| 427 function escapeChar(match) | |
| 428 { | |
| 429 return "\\" + match.charCodeAt(0).toString(16) + " "; | |
| 430 } | |
| 431 | |
| 432 // Return CSS data | |
| 433 let cssTemplate = "-moz-binding: url(about:abp-elemhidehit?%ID%#dummy) !impo
rtant;"; | |
| 434 for (let domain in domains) | |
| 435 { | |
| 436 let rules = []; | |
| 437 let list = domains[domain]; | |
| 438 | |
| 439 if (domain) | |
| 440 yield ('@-moz-document domain("' + domain.split(",").join('"),domain("')
+ '"){').replace(/[^\x01-\x7F]/g, escapeChar); | |
| 441 else | |
| 442 { | |
| 443 // Only allow unqualified rules on a few protocols to prevent them from
blocking chrome | |
| 444 yield '@-moz-document url-prefix("http://"),url-prefix("https://"),' | |
| 445 + 'url-prefix("mailbox://"),url-prefix("imap://"),' | |
| 446 + 'url-prefix("news://"),url-prefix("snews://"){'; | |
| 447 } | |
| 448 | |
| 449 for (let selector in list) | |
| 450 yield selector.replace(/[^\x01-\x7F]/g, escapeChar) + "{" + cssTemplate.
replace("%ID%", list[selector]) + "}"; | |
| 451 yield '}'; | |
| 452 } | |
| 453 }, | |
| 454 | |
| 455 /** | |
| 456 * Unapplies current stylesheet URL | |
| 457 */ | |
| 458 unapply: function() | |
| 459 { | |
| 460 if (ElemHide.applied) | |
| 461 { | |
| 462 try | |
| 463 { | |
| 464 Utils.styleService.unregisterSheet(styleURL, Ci.nsIStyleSheetService.USE
R_SHEET); | |
| 465 } | |
| 466 catch (e) | |
| 467 { | |
| 468 Cu.reportError(e); | |
| 469 } | |
| 470 ElemHide.applied = false; | |
| 471 } | |
| 472 }, | |
| 473 | |
| 474 /** | |
| 475 * Retrieves the currently applied stylesheet URL | |
| 476 * @type String | |
| 477 */ | |
| 478 get styleURL() | |
| 479 { | |
| 480 return ElemHide.applied ? styleURL.spec : null; | |
| 481 }, | |
| 482 | |
| 483 /** | |
| 484 * Retrieves an element hiding filter by the corresponding protocol key | 266 * Retrieves an element hiding filter by the corresponding protocol key |
| 485 */ | 267 */ |
| 486 getFilterByKey: function(/**String*/ key) /**Filter*/ | 268 getFilterByKey: function(/**String*/ key) /**Filter*/ |
| 487 { | 269 { |
| 488 return (key in filterByKey ? filterByKey[key] : null); | 270 return (key in filterByKey ? filterByKey[key] : null); |
| 489 }, | 271 }, |
| 490 | 272 |
| 491 /** | 273 /** |
| 274 * Returns a list of all selectors as a nested map. On first level, the keys |
| 275 * are all values of `ElemHideBase.selectorDomain` (domains on which these |
| 276 * selectors should apply, ignoring exceptions). The values are maps again, |
| 277 * with the keys being selectors and values the corresponding filter keys. |
| 278 * @returns {Map.<String,Map<String,String>>} |
| 279 */ |
| 280 getSelectors: function() |
| 281 { |
| 282 let domains = new Map(); |
| 283 for (let key in filterByKey) |
| 284 { |
| 285 let filter = filterByKey[key]; |
| 286 let selector = filter.selector; |
| 287 if (!selector) |
| 288 continue; |
| 289 |
| 290 let domain = filter.selectorDomain || ""; |
| 291 |
| 292 if (!domains.has(domain)) |
| 293 domains.set(domain, new Map()); |
| 294 domains.get(domain).set(selector, key); |
| 295 } |
| 296 |
| 297 return domains; |
| 298 }, |
| 299 |
| 300 /** |
| 492 * Returns a list of all selectors active on a particular domain, must not be | 301 * Returns a list of all selectors active on a particular domain, must not be |
| 493 * used in Firefox (when usingGetSelectorsForDomain is false). | 302 * used in Firefox (when usingGetSelectorsForDomain is false). |
| 494 */ | 303 */ |
| 495 getSelectorsForDomain: function(/**String*/ domain, /**Boolean*/ specificOnly) | 304 getSelectorsForDomain: function(/**String*/ domain, /**Boolean*/ specificOnly) |
| 496 { | 305 { |
| 497 if (!usingGetSelectorsForDomain) | 306 if (!usingGetSelectorsForDomain) |
| 498 throw new Error("getSelectorsForDomain can not be used in Firefox!"); | 307 throw new Error("getSelectorsForDomain can not be used in Firefox!"); |
| 499 | 308 |
| 500 if (!unconditionalSelectors) | 309 if (!unconditionalSelectors) |
| 501 unconditionalSelectors = Object.keys(filtersBySelector); | 310 unconditionalSelectors = Object.keys(filtersBySelector); |
| (...skipping 24 matching lines...) Expand all Loading... |
| 526 if (currentDomain == "") | 335 if (currentDomain == "") |
| 527 break; | 336 break; |
| 528 | 337 |
| 529 let nextDot = currentDomain.indexOf("."); | 338 let nextDot = currentDomain.indexOf("."); |
| 530 currentDomain = nextDot == -1 ? "" : currentDomain.substr(nextDot + 1); | 339 currentDomain = nextDot == -1 ? "" : currentDomain.substr(nextDot + 1); |
| 531 } | 340 } |
| 532 | 341 |
| 533 return selectors; | 342 return selectors; |
| 534 } | 343 } |
| 535 }; | 344 }; |
| OLD | NEW |