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 |