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 |