| 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 | 
|---|