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 Handles notifications. | |
20 */ | |
21 | |
22 Cu.import("resource://gre/modules/Services.jsm"); | |
23 | |
24 var {Prefs} = require("prefs"); | |
25 var {Downloader, Downloadable, MILLIS_IN_MINUTE, MILLIS_IN_HOUR, MILLIS_IN_DAY}
= require("downloader"); | |
26 var {Utils} = require("utils"); | |
27 var {Matcher, defaultMatcher} = require("matcher"); | |
28 var {Filter, RegExpFilter, WhitelistFilter} = require("filterClasses"); | |
29 | |
30 var INITIAL_DELAY = 1 * MILLIS_IN_MINUTE; | |
31 var CHECK_INTERVAL = 1 * MILLIS_IN_HOUR; | |
32 var EXPIRATION_INTERVAL = 1 * MILLIS_IN_DAY; | |
33 var TYPE = { | |
34 information: 0, | |
35 question: 1, | |
36 critical: 2 | |
37 }; | |
38 | |
39 var showListeners = []; | |
40 var questionListeners = {}; | |
41 | |
42 function getNumericalSeverity(notification) | |
43 { | |
44 return (notification.type in TYPE ? TYPE[notification.type] : TYPE.information
); | |
45 } | |
46 | |
47 function saveNotificationData() | |
48 { | |
49 // HACK: JSON values aren't saved unless they are assigned a different object. | |
50 Prefs.notificationdata = JSON.parse(JSON.stringify(Prefs.notificationdata)); | |
51 } | |
52 | |
53 function localize(translations, locale) | |
54 { | |
55 if (locale in translations) | |
56 return translations[locale]; | |
57 | |
58 let languagePart = locale.substring(0, locale.indexOf("-")); | |
59 if (languagePart && languagePart in translations) | |
60 return translations[languagePart]; | |
61 | |
62 let defaultLocale = "en-US"; | |
63 return translations[defaultLocale]; | |
64 } | |
65 | |
66 /** | |
67 * The object providing actual downloading functionality. | |
68 * @type Downloader | |
69 */ | |
70 var downloader = null; | |
71 var localData = []; | |
72 | |
73 /** | |
74 * Regularly fetches notifications and decides which to show. | |
75 * @class | |
76 */ | |
77 var Notification = exports.Notification = | |
78 { | |
79 /** | |
80 * Called on module startup. | |
81 */ | |
82 init: function() | |
83 { | |
84 downloader = new Downloader(this._getDownloadables.bind(this), INITIAL_DELAY
, CHECK_INTERVAL); | |
85 downloader.onExpirationChange = this._onExpirationChange.bind(this); | |
86 downloader.onDownloadSuccess = this._onDownloadSuccess.bind(this); | |
87 downloader.onDownloadError = this._onDownloadError.bind(this); | |
88 onShutdown.add(() => downloader.cancel()); | |
89 }, | |
90 | |
91 /** | |
92 * Yields a Downloadable instances for the notifications download. | |
93 */ | |
94 _getDownloadables: function*() | |
95 { | |
96 let downloadable = new Downloadable(Prefs.notificationurl); | |
97 if (typeof Prefs.notificationdata.lastError === "number") | |
98 downloadable.lastError = Prefs.notificationdata.lastError; | |
99 if (typeof Prefs.notificationdata.lastCheck === "number") | |
100 downloadable.lastCheck = Prefs.notificationdata.lastCheck; | |
101 if (typeof Prefs.notificationdata.data === "object" && "version" in Prefs.no
tificationdata.data) | |
102 downloadable.lastVersion = Prefs.notificationdata.data.version; | |
103 if (typeof Prefs.notificationdata.softExpiration === "number") | |
104 downloadable.softExpiration = Prefs.notificationdata.softExpiration; | |
105 if (typeof Prefs.notificationdata.hardExpiration === "number") | |
106 downloadable.hardExpiration = Prefs.notificationdata.hardExpiration; | |
107 if (typeof Prefs.notificationdata.downloadCount === "number") | |
108 downloadable.downloadCount = Prefs.notificationdata.downloadCount; | |
109 yield downloadable; | |
110 }, | |
111 | |
112 _onExpirationChange: function(downloadable) | |
113 { | |
114 Prefs.notificationdata.lastCheck = downloadable.lastCheck; | |
115 Prefs.notificationdata.softExpiration = downloadable.softExpiration; | |
116 Prefs.notificationdata.hardExpiration = downloadable.hardExpiration; | |
117 saveNotificationData(); | |
118 }, | |
119 | |
120 _onDownloadSuccess: function(downloadable, responseText, errorCallback, redire
ctCallback) | |
121 { | |
122 try | |
123 { | |
124 let data = JSON.parse(responseText); | |
125 for (let notification of data.notifications) | |
126 { | |
127 if ("severity" in notification) | |
128 { | |
129 if (!("type" in notification)) | |
130 notification.type = notification.severity; | |
131 delete notification.severity; | |
132 } | |
133 } | |
134 Prefs.notificationdata.data = data; | |
135 } | |
136 catch (e) | |
137 { | |
138 Cu.reportError(e); | |
139 errorCallback("synchronize_invalid_data"); | |
140 return; | |
141 } | |
142 | |
143 Prefs.notificationdata.lastError = 0; | |
144 Prefs.notificationdata.downloadStatus = "synchronize_ok"; | |
145 [Prefs.notificationdata.softExpiration, Prefs.notificationdata.hardExpiratio
n] = downloader.processExpirationInterval(EXPIRATION_INTERVAL); | |
146 Prefs.notificationdata.downloadCount = downloadable.downloadCount; | |
147 saveNotificationData(); | |
148 | |
149 Notification.showNext(); | |
150 }, | |
151 | |
152 _onDownloadError: function(downloadable, downloadURL, error, channelStatus, re
sponseStatus, redirectCallback) | |
153 { | |
154 Prefs.notificationdata.lastError = Date.now(); | |
155 Prefs.notificationdata.downloadStatus = error; | |
156 saveNotificationData(); | |
157 }, | |
158 | |
159 /** | |
160 * Adds a listener for notifications to be shown. | |
161 * @param {Function} listener Listener to be invoked when a notification is | |
162 * to be shown | |
163 */ | |
164 addShowListener: function(listener) | |
165 { | |
166 if (showListeners.indexOf(listener) == -1) | |
167 showListeners.push(listener); | |
168 }, | |
169 | |
170 /** | |
171 * Removes the supplied listener. | |
172 * @param {Function} listener Listener that was added via addShowListener() | |
173 */ | |
174 removeShowListener: function(listener) | |
175 { | |
176 let index = showListeners.indexOf(listener); | |
177 if (index != -1) | |
178 showListeners.splice(index, 1); | |
179 }, | |
180 | |
181 /** | |
182 * Determines which notification is to be shown next. | |
183 * @param {String} url URL to match notifications to (optional) | |
184 * @return {Object} notification to be shown, or null if there is none | |
185 */ | |
186 _getNextToShow: function(url) | |
187 { | |
188 function checkTarget(target, parameter, name, version) | |
189 { | |
190 let minVersionKey = parameter + "MinVersion"; | |
191 let maxVersionKey = parameter + "MaxVersion"; | |
192 return !((parameter in target && target[parameter] != name) || | |
193 (minVersionKey in target && Services.vc.compare(version, target[m
inVersionKey]) < 0) || | |
194 (maxVersionKey in target && Services.vc.compare(version, target[m
axVersionKey]) > 0)); | |
195 } | |
196 | |
197 let remoteData = []; | |
198 if (typeof Prefs.notificationdata.data == "object" && Prefs.notificationdata
.data.notifications instanceof Array) | |
199 remoteData = Prefs.notificationdata.data.notifications; | |
200 | |
201 let notifications = localData.concat(remoteData); | |
202 if (notifications.length === 0) | |
203 return null; | |
204 | |
205 let {addonName, addonVersion, application, applicationVersion, platform, pla
tformVersion} = require("info"); | |
206 let notificationToShow = null; | |
207 for (let notification of notifications) | |
208 { | |
209 if (typeof notification.type === "undefined" || notification.type !== "cri
tical") | |
210 { | |
211 let shown = Prefs.notificationdata.shown; | |
212 if (shown instanceof Array && shown.indexOf(notification.id) != -1) | |
213 continue; | |
214 if (Prefs.notifications_ignoredcategories.indexOf("*") != -1) | |
215 continue; | |
216 } | |
217 | |
218 if (typeof url === "string" || notification.urlFilters instanceof Array) | |
219 { | |
220 if (Prefs.enabled && typeof url === "string" && notification.urlFilters
instanceof Array) | |
221 { | |
222 let host; | |
223 if (typeof URL == "function") | |
224 host = new URL(url).hostname; | |
225 else | |
226 { | |
227 try | |
228 { | |
229 host = Services.io.newURI(url, null, null).host; | |
230 } | |
231 catch (e) | |
232 { | |
233 // Ignore, an exception is expected for about: and similar schemes | |
234 host = ""; | |
235 } | |
236 } | |
237 let exception = defaultMatcher.matchesAny(url, RegExpFilter.typeMap.DO
CUMENT, host, false, null); | |
238 if (exception instanceof WhitelistFilter) | |
239 continue; | |
240 | |
241 let matcher = new Matcher(); | |
242 for (let urlFilter of notification.urlFilters) | |
243 matcher.add(Filter.fromText(urlFilter)); | |
244 if (!matcher.matchesAny(url, RegExpFilter.typeMap.DOCUMENT, host, fals
e, null)) | |
245 continue; | |
246 } | |
247 else | |
248 continue; | |
249 } | |
250 | |
251 if (notification.targets instanceof Array) | |
252 { | |
253 let match = false; | |
254 for (let target of notification.targets) | |
255 { | |
256 if (checkTarget(target, "extension", addonName, addonVersion) && | |
257 checkTarget(target, "application", application, applicationVersion
) && | |
258 checkTarget(target, "platform", platform, platformVersion)) | |
259 { | |
260 match = true; | |
261 break; | |
262 } | |
263 } | |
264 if (!match) | |
265 continue; | |
266 } | |
267 | |
268 if (!notificationToShow | |
269 || getNumericalSeverity(notification) > getNumericalSeverity(notificat
ionToShow)) | |
270 notificationToShow = notification; | |
271 } | |
272 | |
273 return notificationToShow; | |
274 }, | |
275 | |
276 /** | |
277 * Invokes the listeners added via addShowListener() with the next | |
278 * notification to be shown. | |
279 * @param {String} url URL to match notifications to (optional) | |
280 */ | |
281 showNext: function(url) | |
282 { | |
283 let notification = Notification._getNextToShow(url); | |
284 if (notification) | |
285 for (let showListener of showListeners) | |
286 showListener(notification); | |
287 }, | |
288 | |
289 /** | |
290 * Marks a notification as shown. | |
291 * @param {String} id ID of the notification to be marked as shown | |
292 */ | |
293 markAsShown: function(id) | |
294 { | |
295 var data = Prefs.notificationdata; | |
296 | |
297 if (!(data.shown instanceof Array)) | |
298 data.shown = []; | |
299 if (data.shown.indexOf(id) != -1) | |
300 return; | |
301 | |
302 data.shown.push(id); | |
303 saveNotificationData(); | |
304 }, | |
305 | |
306 /** | |
307 * Localizes the texts of the supplied notification. | |
308 * @param {Object} notification notification to translate | |
309 * @param {String} locale the target locale (optional, defaults to the | |
310 * application locale) | |
311 * @return {Object} the translated texts | |
312 */ | |
313 getLocalizedTexts: function(notification, locale) | |
314 { | |
315 locale = locale || Utils.appLocale; | |
316 let textKeys = ["title", "message"]; | |
317 let localizedTexts = []; | |
318 for (let key of textKeys) | |
319 { | |
320 if (key in notification) | |
321 { | |
322 if (typeof notification[key] == "string") | |
323 localizedTexts[key] = notification[key]; | |
324 else | |
325 localizedTexts[key] = localize(notification[key], locale); | |
326 } | |
327 } | |
328 return localizedTexts; | |
329 }, | |
330 | |
331 /** | |
332 * Adds a local notification. | |
333 * @param {Object} notification notification to add | |
334 */ | |
335 addNotification: function(notification) | |
336 { | |
337 if (localData.indexOf(notification) == -1) | |
338 localData.push(notification); | |
339 }, | |
340 | |
341 /** | |
342 * Removes an existing local notification. | |
343 * @param {Object} notification notification to remove | |
344 */ | |
345 removeNotification: function(notification) | |
346 { | |
347 let index = localData.indexOf(notification); | |
348 if (index > -1) | |
349 localData.splice(index, 1); | |
350 }, | |
351 | |
352 /** | |
353 * Adds a listener for question-type notifications | |
354 */ | |
355 addQuestionListener: function(/**string*/ id, /**function(approved)*/ listener
) | |
356 { | |
357 if (!(id in questionListeners)) | |
358 questionListeners[id] = []; | |
359 if (questionListeners[id].indexOf(listener) === -1) | |
360 questionListeners[id].push(listener); | |
361 }, | |
362 | |
363 /** | |
364 * Removes a listener that was previously added via addQuestionListener | |
365 */ | |
366 removeQuestionListener: function(/**string*/ id, /**function(approved)*/ liste
ner) | |
367 { | |
368 if (!(id in questionListeners)) | |
369 return; | |
370 let index = questionListeners[id].indexOf(listener); | |
371 if (index > -1) | |
372 questionListeners[id].splice(index, 1); | |
373 if (questionListeners[id].length === 0) | |
374 delete questionListeners[id]; | |
375 }, | |
376 | |
377 /** | |
378 * Notifies question listeners about interactions with a notification | |
379 * @param {String} id notification ID | |
380 * @param {Boolean} approved indicator whether notification has been approved
or not | |
381 */ | |
382 triggerQuestionListeners: function(id, approved) | |
383 { | |
384 if (!(id in questionListeners)) | |
385 return; | |
386 let listeners = questionListeners[id]; | |
387 for (let listener of listeners) | |
388 listener(approved); | |
389 }, | |
390 | |
391 /** | |
392 * Toggles whether notifications of a specific category should be ignored | |
393 * @param {String} category notification category identifier | |
394 * @param {Boolean} [forceValue] force specified value | |
395 */ | |
396 toggleIgnoreCategory: function(category, forceValue) | |
397 { | |
398 let categories = Prefs.notifications_ignoredcategories; | |
399 let index = categories.indexOf(category); | |
400 if (index == -1 && forceValue !== false) | |
401 { | |
402 categories.push(category); | |
403 Prefs.notifications_showui = true; | |
404 } | |
405 else if (index != -1 && forceValue !== true) | |
406 categories.splice(index, 1); | |
407 | |
408 // HACK: JSON values aren't saved unless they are assigned a different objec
t. | |
409 Prefs.notifications_ignoredcategories = JSON.parse(JSON.stringify(categories
)); | |
410 } | |
411 }; | |
412 Notification.init(); | |
OLD | NEW |