| LEFT | RIGHT | 
|---|
| 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 let {Services} = Cu.import("resource://gre/modules/Services.jsm", null); | 18 let {Services} = Cu.import("resource://gre/modules/Services.jsm", null); | 
| 19 let {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm", null); | 19 let {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm", null); | 
| 20 let {setTimeout, clearTimeout} = Cu.import("resource://gre/modules/Timer.jsm", n
     ull); | 20 let {setTimeout, clearTimeout} = Cu.import("resource://gre/modules/Timer.jsm", n
     ull); | 
| 21 | 21 | 
| 22 let {IO} = require("io"); | 22 let {IO} = require("io"); | 
| 23 let {Prefs} = require("prefs"); | 23 let {Prefs} = require("prefs"); | 
| 24 let {Downloader, Downloadable, MILLIS_IN_SECOND, MILLIS_IN_DAY, MILLIS_IN_HOUR} 
     = require("downloader"); | 24 let {Downloader, Downloadable, MILLIS_IN_SECOND, MILLIS_IN_MINUTE, | 
|  | 25   MILLIS_IN_HOUR, MILLIS_IN_DAY} = require("downloader"); | 
| 25 let {FilterNotifier} = require("filterNotifier"); | 26 let {FilterNotifier} = require("filterNotifier"); | 
| 26 let {DownloadableSubscription} = require("subscriptionClasses"); | 27 let {DownloadableSubscription} = require("subscriptionClasses"); | 
| 27 | 28 | 
| 28 const CHECK_INTERVAL = MILLIS_IN_DAY; | 29 const CHECK_INTERVAL = MILLIS_IN_HOUR; | 
| 29 const INITIAL_DELAY = MILLIS_IN_SECOND * 5; | 30 const INITIAL_DELAY = MILLIS_IN_MINUTE * 2; | 
| 30 const PUSH_INTERVAL = MILLIS_IN_DAY * 7; | 31 const PUSH_INTERVAL = MILLIS_IN_DAY * 7; | 
| 31 | 32 | 
| 32 /** | 33 /** | 
| 33  * The value of storage statement normal execution constant | 34  * The value of storage statement normal execution constant | 
| 34  * @type Number | 35  * @type Number | 
| 35  */ | 36  */ | 
| 36 const REASON_FINISHED = Components.interfaces.mozIStorageStatementCallback.REASO
     N_FINISHED; | 37 const REASON_FINISHED = Components.interfaces.mozIStorageStatementCallback.REASO
     N_FINISHED; | 
| 37 | 38 | 
| 38 /** | 39 /** | 
| 39  * This class collects filter hits statistics in a SQLite database | 40  * This class collects filter hits statistics in a SQLite database | 
| 40  * and sends them to the server when user opt-in for that | 41  * and sends them to the server when user opt-in for that | 
| 41  * @class | 42  * @class | 
| 42  */ | 43  */ | 
| 43 let FilterHits = exports.FilterHits = | 44 let FilterHits = exports.FilterHits = | 
| 44 { | 45 { | 
| 45   /** | 46   /** | 
| 46    * Used to prevent timers running in parallel | 47    * Used to prevent timers running in parallel | 
| 47    * @type Number | 48    * @type Number | 
| 48    */ | 49    */ | 
| 49   _prefToggleTimeoutId: 0, | 50   _prefToggleTimeout: 0, | 
| 50 |  | 
| 51   /** |  | 
| 52    * Indicates whether the data is being sent to the server |  | 
| 53    * @type Boolean |  | 
| 54    */ |  | 
| 55   _sending: false, |  | 
| 56 | 51 | 
| 57   get connection() | 52   get connection() | 
| 58   { | 53   { | 
| 59     let connection = Services.storage.openDatabase(this.getStorageFile); | 54     if (!this.storageFile) | 
|  | 55       return null; | 
|  | 56 | 
|  | 57     let connection = Services.storage.openDatabase(this.storageFile); | 
| 60     if (!connection.tableExists("filtersubscriptions")) | 58     if (!connection.tableExists("filtersubscriptions")) | 
| 61     { | 59     { | 
| 62       connection.executeSimpleSQL("CREATE TABLE filtersubscriptions " + | 60       connection.executeSimpleSQL("CREATE TABLE filtersubscriptions " + | 
| 63         "(id INTEGER, subscriptions TEXT, PRIMARY KEY" + | 61         "(id INTEGER, subscriptions TEXT, PRIMARY KEY" + | 
| 64         "(id), UNIQUE(subscriptions))"); | 62         "(id), UNIQUE(subscriptions))"); | 
| 65     } | 63     } | 
| 66     if (!connection.tableExists("filterhits")) | 64     if (!connection.tableExists("filterhits")) | 
| 67     { | 65     { | 
| 68       connection.executeSimpleSQL("CREATE TABLE filterhits (filter TEXT, " + | 66       connection.executeSimpleSQL("CREATE TABLE filterhits (filter TEXT, " + | 
| 69         "host TEXT, thirdParty INTEGER, hitCount INTEGER, lastHit INTEGER, " + | 67         "host TEXT, thirdParty INTEGER, hitCount INTEGER, lastHit INTEGER, " + | 
| 70         "subscriptions INTEGER NOT NULL, PRIMARY KEY(filter, host, " + | 68         "subscriptions INTEGER NOT NULL, PRIMARY KEY(filter, host, " + | 
| 71         "thirdParty), FOREIGN KEY(subscriptions) REFERENCES "+ | 69         "thirdParty), FOREIGN KEY(subscriptions) REFERENCES "+ | 
| 72         "filtersubscriptions(id))"); | 70         "filtersubscriptions(id))"); | 
| 73       Prefs.sendstats_last_push = Date.now(); |  | 
| 74     } | 71     } | 
| 75     Object.defineProperty(this, "connection", | 72     Object.defineProperty(this, "connection", | 
| 76     { | 73     { | 
| 77       value: connection | 74       value: connection | 
| 78     }); | 75     }); | 
| 79     return connection; | 76     return connection; | 
| 80   }, | 77   }, | 
| 81 | 78 | 
| 82   /** | 79   /** | 
| 83    * @return nsIFile SQLite database file with filter hits data | 80    * @return nsIFile SQLite database file with filter hits data | 
| 84    */ | 81    */ | 
| 85   get getStorageFile() | 82   get storageFile() | 
| 86   { | 83   { | 
| 87     let file = IO.resolveFilePath(Prefs.data_directory); | 84     let file = IO.resolveFilePath(Prefs.data_directory); | 
| 88     if (file) | 85     if (file) | 
| 89       file.append("adblockplus.sqlite"); | 86       file.append("adblockplus.sqlite"); | 
| 90 | 87 | 
| 91     Object.defineProperty(this, "getStorageFile", | 88     Object.defineProperty(this, "storageFile", | 
| 92     { | 89     { | 
| 93       value: file | 90       value: file | 
| 94     }); | 91     }); | 
| 95     return file; | 92     return file; | 
| 96   }, | 93   }, | 
| 97 | 94 | 
| 98   /** | 95   /** | 
| 99    * Called on module startup. | 96    * Called on module startup. | 
| 100    */ | 97    */ | 
| 101   init: function() | 98   init: function() | 
| 102   { | 99   { | 
|  | 100     if (!Prefs.sendstats_status.lastPush) | 
|  | 101     { | 
|  | 102       Prefs.sendstats_status.lastPush = Date.now(); | 
|  | 103       this._saveFilterHitPrefs(); | 
|  | 104     } | 
| 103     Prefs.addListener(name => | 105     Prefs.addListener(name => | 
| 104     { | 106     { | 
| 105       if (name == "sendstats") | 107       if (name == "sendstats") | 
| 106       { | 108       { | 
| 107         if (this._prefToggleTimeout) | 109         if (this._prefToggleTimeout) | 
| 108           clearTimeout(this._prefToggleTimeout); | 110           clearTimeout(this._prefToggleTimeout); | 
| 109         this._prefToggleTimeout = setTimeout(() => | 111         this._prefToggleTimeout = setTimeout(() => | 
| 110         { | 112         { | 
| 111           if (!Prefs.sendstats) | 113           if (!Prefs.sendstats) | 
| 112             this.resetFilterHits(); | 114             this.resetFilterHits(); | 
| 113         }, 1000); | 115         }, 1000); | 
| 114       } | 116       } | 
| 115     }); | 117     }); | 
| 116 | 118 | 
| 117     let downloader = new Downloader(this._getDownloadables.bind(this), | 119     let downloader = new Downloader(this._getDownloadables.bind(this), | 
| 118       INITIAL_DELAY, CHECK_INTERVAL, this.sendFilterHitsToServer); | 120       INITIAL_DELAY, CHECK_INTERVAL); | 
| 119     downloader.onExpirationChange = this._onExpirationChange.bind(this); | 121     downloader.onExpirationChange = this._onExpirationChange.bind(this); | 
|  | 122     downloader.generateRequestData = this._generateFilterHitsData.bind(this); | 
|  | 123     downloader.onDownloadSuccess = this._onDownloadSuccess.bind(this); | 
|  | 124     downloader.validResponses.push(204); | 
| 120     onShutdown.add(() => downloader.cancel()); | 125     onShutdown.add(() => downloader.cancel()); | 
| 121   }, | 126   }, | 
| 122 | 127 | 
| 123   /** | 128   /** | 
| 124    * Yields a Downloadable instances for the notifications download. | 129    * Yields a Downloadable instance for the filter hits push. | 
| 125    */ | 130    */ | 
| 126   _getDownloadables: function*() | 131   _getDownloadables: function*() | 
| 127   { | 132   { | 
| 128     let downloadable = new Downloadable(); | 133     let downloadable = new Downloadable(Prefs.sendstats_url); | 
| 129     if (typeof Prefs.sendstats_status.lastError === "number") | 134     if (Prefs.sendstats_status.lastError) | 
| 130       downloadable.lastError = Prefs.sendstats_status.lastError; | 135       downloadable.lastError = Prefs.sendstats_status.lastError; | 
| 131     if (typeof Prefs.sendstats_status.lastCheck === "number") | 136     if (Prefs.sendstats_status.lastCheck) | 
| 132       downloadable.lastCheck = Prefs.sendstats_status.lastCheck; | 137       downloadable.lastCheck = Prefs.sendstats_status.lastCheck; | 
| 133     if (typeof Prefs.sendstats_status.softExpiration === "number") | 138     if (Prefs.sendstats_status.lastPush) | 
| 134       downloadable.softExpiration = Prefs.sendstats_status.softExpiration; | 139     { | 
| 135     if (typeof Prefs.sendstats_status.hardExpiration === "number") | 140       downloadable.softExpiration = Prefs.sendstats_status.lastPush + PUSH_INTER
     VAL; | 
| 136       downloadable.hardExpiration = Prefs.sendstats_status.hardExpiration; | 141       downloadable.hardExpiration = Prefs.sendstats_status.lastPush + PUSH_INTER
     VAL; | 
|  | 142     } | 
| 137     yield downloadable; | 143     yield downloadable; | 
| 138   }, | 144   }, | 
| 139 | 145 | 
| 140   _onExpirationChange: function(downloadable) | 146   _onExpirationChange: function(downloadable) | 
| 141   { | 147   { | 
| 142     Prefs.sendstats_status.lastCheck = downloadable.lastCheck; | 148     Prefs.sendstats_status.lastCheck = downloadable.lastCheck; | 
| 143     Prefs.sendstats_status.softExpiration = downloadable.softExpiration; | 149     this._saveFilterHitPrefs(); | 
| 144     Prefs.sendstats_status.hardExpiration = downloadable.hardExpiration; | 150   }, | 
|  | 151 | 
|  | 152   _saveFilterHitPrefs: function() | 
|  | 153   { | 
|  | 154     Prefs.sendstats_status = JSON.parse(JSON.stringify(Prefs.sendstats_status)); | 
|  | 155   }, | 
|  | 156 | 
|  | 157   _onDownloadSuccess: function(downloadable, responseText, errorCallback, redire
     ctCallback) | 
|  | 158   { | 
|  | 159     Prefs.sendstats_status.lastError = 0; | 
|  | 160     Prefs.sendstats_status.lastPush = Date.now(); | 
|  | 161     this._saveFilterHitPrefs(); | 
| 145   }, | 162   }, | 
| 146 | 163 | 
| 147   /** | 164   /** | 
| 148    * Increases the filter hit count | 165    * Increases the filter hit count | 
| 149    * @param {Filter} filter | 166    * @param {Filter} filter | 
| 150    * @param {String} host | 167    * @param {String} host | 
| 151    */ | 168    */ | 
| 152   increaseFilterHits: function(filter, host, thirdParty) | 169   increaseFilterHits: function(filter, host, thirdParty) | 
| 153   { | 170   { | 
| 154     let subscriptions = filter.subscriptions; | 171     let subscriptions = filter.subscriptions; | 
| 155     let downloadableSubscriptions = []; | 172     let downloadableSubscriptions = []; | 
| 156     for (let i = 0; i < subscriptions.length; i++) | 173     for (let i = 0; i < subscriptions.length; i++) | 
| 157     { | 174     { | 
| 158       if (subscriptions[i] instanceof DownloadableSubscription) | 175       if (subscriptions[i] instanceof DownloadableSubscription) | 
| 159         downloadableSubscriptions.push(subscriptions[i].url); | 176         downloadableSubscriptions.push(subscriptions[i].url); | 
| 160     } | 177     } | 
| 161 | 178 | 
| 162     if (downloadableSubscriptions.length == 0) | 179     if (downloadableSubscriptions.length == 0) | 
| 163       return; | 180       return; | 
| 164 | 181 | 
| 165     if (!this.getStorageFile) | 182     if (!this.connection) | 
| 166       return; | 183       return; | 
| 167 | 184 | 
| 168     let statementsArray = []; | 185     let statements = []; | 
| 169     let filtersubscriptions = JSON.stringify(downloadableSubscriptions); | 186     let filterSubscriptions = JSON.stringify(downloadableSubscriptions); | 
| 170     let subscriptionStatement = this.connection.createStatement("INSERT OR " + | 187     let subscriptionStatement = this.connection.createStatement("INSERT OR " + | 
| 171       "IGNORE INTO filtersubscriptions (subscriptions) VALUES " + | 188       "IGNORE INTO filtersubscriptions (subscriptions) VALUES " + | 
| 172       "(:subscriptions)"); | 189       "(:subscriptions)"); | 
| 173     subscriptionStatement.params.subscriptions = filtersubscriptions; | 190     subscriptionStatement.params.subscriptions = filterSubscriptions; | 
| 174     statementsArray.push(subscriptionStatement); | 191     statements.push(subscriptionStatement); | 
| 175     let filterHitstatement = this.connection.createStatement("INSERT OR " + | 192     let filterHitStatement = this.connection.createStatement("INSERT OR " + | 
| 176       "REPLACE INTO filterhits (filter, host, thirdParty, hitCount, lastHit, " + | 193       "REPLACE INTO filterhits (filter, host, thirdParty, hitCount, lastHit, " + | 
| 177       "subscriptions) VALUES (:filter, :host, :thirdParty, coalesce ((SELECT " + | 194       "subscriptions) VALUES (:filter, :host, :thirdParty, COALESCE ((SELECT " + | 
| 178       "hitCount  FROM filterhits WHERE filter=:filter AND host=:host AND " + | 195       "hitCount  FROM filterhits WHERE filter=:filter AND host=:host AND " + | 
| 179       "thirdParty=:thirdParty), 0) + 1, :lastHit, (SELECT id " + | 196       "thirdParty=:thirdParty), 0) + 1, :lastHit, (SELECT id " + | 
| 180       "FROM filtersubscriptions WHERE subscriptions=:subscriptions))"); | 197       "FROM filtersubscriptions WHERE subscriptions=:subscriptions))"); | 
| 181     filterHitstatement.params.filter = filter.text; | 198     filterHitStatement.params.filter = filter.text; | 
| 182     filterHitstatement.params.host = host; | 199     filterHitStatement.params.host = host; | 
| 183     filterHitstatement.params.lastHit = roundToHours(filter.lastHit); | 200     filterHitStatement.params.lastHit = roundToHours(filter.lastHit); | 
| 184     filterHitstatement.params.thirdParty = thirdParty; | 201     filterHitStatement.params.thirdParty = thirdParty; | 
| 185     filterHitstatement.params.subscriptions = filtersubscriptions; | 202     filterHitStatement.params.subscriptions = filterSubscriptions; | 
| 186     statementsArray.push(filterHitstatement); | 203     statements.push(filterHitStatement); | 
| 187 | 204 | 
| 188     this.connection.executeAsync(statementsArray, statementsArray.length, | 205     this.connection.executeAsync(statements, statements.length, | 
| 189     { | 206     { | 
| 190       handleError: function(error) | 207       handleError: (error) => | 
| 191       { | 208       { | 
| 192         Cu.reportError("Error during writing of filter hits: " + reason); | 209         Cu.reportError("Error updating filter hits: " + error.message); | 
| 193       }, | 210       }, | 
| 194       handleCompletion: function(reason) | 211       handleCompletion: (reason) => | 
| 195       { | 212       { | 
| 196         if (reason != REASON_FINISHED) | 213         if (reason != REASON_FINISHED) | 
| 197           Cu.reportError("Writing of filter hits canceled or aborted: " + reason
     ); | 214           Cu.reportError("Updating filter hits canceled or aborted: " + reason); | 
| 198       } | 215       } | 
| 199     }); | 216     }); | 
| 200   }, | 217   }, | 
| 201 | 218 | 
| 202   /** | 219   /** | 
| 203    * Remove all local collected filter hits data | 220    * Remove all local collected filter hits data | 
| 204    */ | 221    */ | 
| 205   resetFilterHits: function() | 222   resetFilterHits: function() | 
| 206   { | 223   { | 
| 207     if (!this.getStorageFile) | 224     if (!this.connection) | 
| 208       return; | 225       return; | 
| 209 | 226 | 
| 210     this.connection.executeSimpleSQL("DELETE FROM filterhits"); | 227     this.connection.executeSimpleSQL("DELETE FROM filterhits"); | 
| 211     this.connection.executeSimpleSQL("DELETE FROM filtersubscriptions"); | 228     this.connection.executeSimpleSQL("DELETE FROM filtersubscriptions"); | 
| 212   }, | 229   }, | 
| 213 | 230 | 
| 214   /** | 231   _generateFilterHitsData: function(downloadable) | 
| 215    * Send local stored filter hit data to the | 232   { | 
| 216    * service specified in Prefs.sendstats_url | 233     return { | 
| 217    */ | 234       then: (resolve) => | 
| 218   sendFilterHitsToServer: function() | 235       { | 
| 219   { | 236         if (!this.connection) | 
| 220     let now = Date.now(); | 237           return; | 
| 221     if (Prefs.sendstats_last_push > now) | 238 | 
| 222       Prefs.sendstats_last_push = now; | 239         let statement = this.connection.createStatement("SELECT filterhits.*, " 
     + | 
| 223 | 240         "filtersubscriptions.subscriptions FROM filterhits LEFT JOIN " + | 
| 224     if ((now - Prefs.sendstats_last_push < PUSH_INTERVAL) || !Prefs.sendstats | 241         "filtersubscriptions filtersubscriptions ON " + | 
| 225         || !this.getStorageFile) | 242         "filterhits.subscriptions=filtersubscriptions.id"); | 
| 226       return; | 243         statement.executeAsync( | 
| 227 |  | 
| 228     let statement = this.connection.createStatement("SELECT filterhits.*, " + |  | 
| 229       "filtersubscriptions.subscriptions FROM filterhits LEFT JOIN " + |  | 
| 230       "filtersubscriptions filtersubscriptions ON " + |  | 
| 231       "filterhits.subscriptions=filtersubscriptions.id"); |  | 
| 232     statement.executeAsync( |  | 
| 233     { |  | 
| 234       handleResult: (result) => |  | 
| 235       { |  | 
| 236         let filters = Object.create(null); |  | 
| 237         for (let row = result.getNextRow(); row; |  | 
| 238              row = result.getNextRow()) |  | 
| 239         { | 244         { | 
| 240           let filterText = row.getResultByName("filter"); | 245           handleResult: (result) => | 
| 241           let host = row.getResultByName("host"); |  | 
| 242           let matchType = (row.getResultByName("thirdParty") ? |  | 
| 243                             "thirdParty" : "firstParty"); |  | 
| 244           let hitCount = row.getResultByName("hitCount"); |  | 
| 245           let lastHit = row.getResultByName("lastHit"); |  | 
| 246           let subscriptions = row.getResultByName("subscriptions"); |  | 
| 247 |  | 
| 248           if (!(filterText in filters)) |  | 
| 249            filters[filterText] = Object.create(null); |  | 
| 250 |  | 
| 251           let filter = filters[filterText]; |  | 
| 252           filter.subscriptions = subscriptions; |  | 
| 253           filter[matchType] = Object.create(null); |  | 
| 254           filter[matchType][host] = {"hits": hitCount, latest: lastHit}; |  | 
| 255         } |  | 
| 256 |  | 
| 257         let request = new XMLHttpRequest(); |  | 
| 258         request.open("POST", Prefs.sendstats_url); |  | 
| 259         request.setRequestHeader("Content-Type", "application/json"); |  | 
| 260         request.addEventListener("load", () => |  | 
| 261         { |  | 
| 262           this._sending = false; |  | 
| 263           if (request.status >= 200 && request.status < 300) |  | 
| 264           { | 246           { | 
| 265             Prefs.sendstats_status.lastError = 0; | 247             let filters = Object.create(null); | 
| 266             Prefs.sendstats_last_push = Date.now(); | 248             for (let row = result.getNextRow(); row; | 
| 267             FilterHits.resetFilterHits(); | 249                  row = result.getNextRow()) | 
| 268           } | 250             { | 
| 269           else | 251               let filterText = row.getResultByName("filter"); | 
|  | 252               let host = row.getResultByName("host"); | 
|  | 253               let matchType = (row.getResultByName("thirdParty") ? | 
|  | 254                                 "thirdParty" : "firstParty"); | 
|  | 255               let hitCount = row.getResultByName("hitCount"); | 
|  | 256               let lastHit = row.getResultByName("lastHit"); | 
|  | 257               let subscriptions = row.getResultByName("subscriptions"); | 
|  | 258 | 
|  | 259               if (!(filterText in filters)) | 
|  | 260                filters[filterText] = Object.create(null); | 
|  | 261 | 
|  | 262               let filter = filters[filterText]; | 
|  | 263               filter.subscriptions = subscriptions; | 
|  | 264               filter[matchType] = Object.create(null); | 
|  | 265               filter[matchType][host] = {"hits": hitCount, latest: lastHit}; | 
|  | 266             } | 
|  | 267             resolve({timeSincePush: roundToHours(Date.now() - | 
|  | 268               Prefs.sendstats_status.lastPush), "filters": filters}); | 
|  | 269           }, | 
|  | 270           handleError: (error) => | 
| 270           { | 271           { | 
| 271             Prefs.sendstats_status.lastError = Date.now(); | 272             Prefs.sendstats_status.lastError = Date.now(); | 
| 272             this._sending = false; | 273             this._saveFilterHitPrefs(); | 
| 273             Cu.reportError("Couldn't send filter hit statistics to server. Statu
     s code: " + request.status); | 274             Cu.reportError("Error loading filter hits:" + error.message); | 
|  | 275           }, | 
|  | 276           handleCompletion: (reason) => | 
|  | 277           { | 
|  | 278             if (reason != REASON_FINISHED) | 
|  | 279             { | 
|  | 280               Prefs.sendstats_status.lastError = Date.now(); | 
|  | 281               this._saveFilterHitPrefs(); | 
|  | 282               Cu.reportError("Loading of filter hits canceled or aborted: " + | 
|  | 283                 reason); | 
|  | 284             } | 
| 274           } | 285           } | 
| 275         }, false); | 286         }); | 
| 276         request.addEventListener("error", () => |  | 
| 277         { |  | 
| 278           this._sending = false; |  | 
| 279           Cu.reportError("Error occured while sending filter hit statistics to s
     erver."); |  | 
| 280         }, false); |  | 
| 281 |  | 
| 282         let {addonName, addonVersion, application, |  | 
| 283             applicationVersion, platform, platformVersion} = require("info"); |  | 
| 284         let data = { |  | 
| 285           version: 1, |  | 
| 286           timeSincePush: roundToHours(Date.now() - Prefs.sendstats_last_push), |  | 
| 287           addonName: addonName, |  | 
| 288           addonVersion: addonVersion, |  | 
| 289           application: application, |  | 
| 290           applicationVersion: applicationVersion, |  | 
| 291           platform: platform, |  | 
| 292           platformVersion: platformVersion, |  | 
| 293           filters: filters |  | 
| 294         }; |  | 
| 295         if (!this._sending) |  | 
| 296         { |  | 
| 297           this._sending = true; |  | 
| 298           request.send(JSON.stringify(data)); |  | 
| 299         } |  | 
| 300       }, |  | 
| 301       handleError: function(error) |  | 
| 302       { |  | 
| 303         Prefs.sendstats_status.lastError = Date.now(); |  | 
| 304         Cu.reportError(error.message); |  | 
| 305       }, |  | 
| 306       handleCompletion: function(reason) |  | 
| 307       { |  | 
| 308         if (reason != REASON_FINISHED) |  | 
| 309         { |  | 
| 310           Prefs.sendstats_status.lastError = Date.now(); |  | 
| 311           Cu.reportError("Loading of filter hits canceled or aborted: " + reason
     ); |  | 
| 312         } |  | 
| 313       } | 287       } | 
| 314     }); | 288     } | 
| 315   }, | 289   }, | 
| 316 }; | 290 }; | 
| 317 | 291 | 
| 318 function roundToHours(timestamp) | 292 function roundToHours(timestamp) | 
| 319 { | 293 { | 
| 320   return Math.round(timestamp / MILLIS_IN_HOUR) * 3600; | 294   return Math.round(timestamp / MILLIS_IN_HOUR) * | 
|  | 295       (MILLIS_IN_HOUR / MILLIS_IN_SECOND); | 
| 321 } | 296 } | 
| 322 | 297 | 
| 323 FilterHits.init(); | 298 FilterHits.init(); | 
| LEFT | RIGHT | 
|---|