| LEFT | RIGHT | 
|---|
| 1 /* | 1 /* | 
| 2  * This file is part of Adblock Plus <http://adblockplus.org/>, | 2  * This file is part of Adblock Plus <https://adblockplus.org/>, | 
| 3  * Copyright (C) 2006-2014 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 | 20 let {setTimeout, clearTimeout} = Cu.import("resource://gre/modules/Timer.jsm", n
     ull); | 
| 21 let {Utils} = require("utils"); | 21 | 
| 22 let {MILLIS_IN_DAY} = require("downloader"); | 22 let {IO} = require("io"); | 
|  | 23 let {Prefs} = require("prefs"); | 
|  | 24 let {Downloader, Downloadable, MILLIS_IN_SECOND, MILLIS_IN_MINUTE, | 
|  | 25   MILLIS_IN_HOUR, MILLIS_IN_DAY} = require("downloader"); | 
|  | 26 let {FilterNotifier} = require("filterNotifier"); | 
|  | 27 let {DownloadableSubscription} = require("subscriptionClasses"); | 
|  | 28 | 
|  | 29 const CHECK_INTERVAL = MILLIS_IN_HOUR; | 
|  | 30 const INITIAL_DELAY = MILLIS_IN_MINUTE * 2; | 
|  | 31 const PUSH_INTERVAL = MILLIS_IN_DAY * 7; | 
| 23 | 32 | 
| 24 /** | 33 /** | 
| 25  * This class reads filter hits statistics from SQLite database, | 34  * The value of storage statement normal execution constant | 
| 26  * manages them in memory and writes them back. | 35  * @type Number | 
|  | 36  */ | 
|  | 37 const REASON_FINISHED = Components.interfaces.mozIStorageStatementCallback.REASO
     N_FINISHED; | 
|  | 38 | 
|  | 39 /** | 
|  | 40  * This class collects filter hits statistics in a SQLite database | 
|  | 41  * and sends them to the server when user opt-in for that | 
| 27  * @class | 42  * @class | 
| 28  */ | 43  */ | 
| 29 let FilterHits = exports.FilterHits = | 44 let FilterHits = exports.FilterHits = | 
| 30 { | 45 { | 
| 31   filters: {}, | 46   /** | 
| 32 | 47    * Used to prevent timers running in parallel | 
| 33   _serviceURL: "", | 48    * @type Number | 
| 34   _lastPush: 0, | 49    */ | 
| 35   _pushInterval: MILLIS_IN_DAY * 7, | 50   _prefToggleTimeout: 0, | 
| 36   _loading: false, | 51 | 
| 37   _saving: false, | 52   get connection() | 
| 38   _sending: false, | 53   { | 
|  | 54     if (!this.storageFile) | 
|  | 55       return null; | 
|  | 56 | 
|  | 57     let connection = Services.storage.openDatabase(this.storageFile); | 
|  | 58     if (!connection.tableExists("filtersubscriptions")) | 
|  | 59     { | 
|  | 60       connection.executeSimpleSQL("CREATE TABLE filtersubscriptions " + | 
|  | 61         "(id INTEGER, subscriptions TEXT, PRIMARY KEY" + | 
|  | 62         "(id), UNIQUE(subscriptions))"); | 
|  | 63     } | 
|  | 64     if (!connection.tableExists("filterhits")) | 
|  | 65     { | 
|  | 66       connection.executeSimpleSQL("CREATE TABLE filterhits (filter TEXT, " + | 
|  | 67         "host TEXT, thirdParty INTEGER, hitCount INTEGER, lastHit INTEGER, " + | 
|  | 68         "subscriptions INTEGER NOT NULL, PRIMARY KEY(filter, host, " + | 
|  | 69         "thirdParty), FOREIGN KEY(subscriptions) REFERENCES "+ | 
|  | 70         "filtersubscriptions(id))"); | 
|  | 71     } | 
|  | 72     Object.defineProperty(this, "connection", | 
|  | 73     { | 
|  | 74       value: connection | 
|  | 75     }); | 
|  | 76     return connection; | 
|  | 77   }, | 
|  | 78 | 
|  | 79   /** | 
|  | 80    * @return nsIFile SQLite database file with filter hits data | 
|  | 81    */ | 
|  | 82   get storageFile() | 
|  | 83   { | 
|  | 84     let file = IO.resolveFilePath(Prefs.data_directory); | 
|  | 85     if (file) | 
|  | 86       file.append("adblockplus.sqlite"); | 
|  | 87 | 
|  | 88     Object.defineProperty(this, "storageFile", | 
|  | 89     { | 
|  | 90       value: file | 
|  | 91     }); | 
|  | 92     return file; | 
|  | 93   }, | 
|  | 94 | 
|  | 95   /** | 
|  | 96    * Called on module startup. | 
|  | 97    */ | 
|  | 98   init: function() | 
|  | 99   { | 
|  | 100     if (!Prefs.sendstats_status.lastPush) | 
|  | 101     { | 
|  | 102       Prefs.sendstats_status.lastPush = Date.now(); | 
|  | 103       this._saveFilterHitPrefs(); | 
|  | 104     } | 
|  | 105     Prefs.addListener(name => | 
|  | 106     { | 
|  | 107       if (name == "sendstats") | 
|  | 108       { | 
|  | 109         if (this._prefToggleTimeout) | 
|  | 110           clearTimeout(this._prefToggleTimeout); | 
|  | 111         this._prefToggleTimeout = setTimeout(() => | 
|  | 112         { | 
|  | 113           if (!Prefs.sendstats) | 
|  | 114             this.resetFilterHits(); | 
|  | 115         }, 1000); | 
|  | 116       } | 
|  | 117     }); | 
|  | 118 | 
|  | 119     let downloader = new Downloader(this._getDownloadables.bind(this), | 
|  | 120       INITIAL_DELAY, CHECK_INTERVAL); | 
|  | 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); | 
|  | 125     onShutdown.add(() => downloader.cancel()); | 
|  | 126   }, | 
|  | 127 | 
|  | 128   /** | 
|  | 129    * Yields a Downloadable instance for the filter hits push. | 
|  | 130    */ | 
|  | 131   _getDownloadables: function*() | 
|  | 132   { | 
|  | 133     let downloadable = new Downloadable(Prefs.sendstats_url); | 
|  | 134     if (Prefs.sendstats_status.lastError) | 
|  | 135       downloadable.lastError = Prefs.sendstats_status.lastError; | 
|  | 136     if (Prefs.sendstats_status.lastCheck) | 
|  | 137       downloadable.lastCheck = Prefs.sendstats_status.lastCheck; | 
|  | 138     if (Prefs.sendstats_status.lastPush) | 
|  | 139     { | 
|  | 140       downloadable.softExpiration = Prefs.sendstats_status.lastPush + PUSH_INTER
     VAL; | 
|  | 141       downloadable.hardExpiration = Prefs.sendstats_status.lastPush + PUSH_INTER
     VAL; | 
|  | 142     } | 
|  | 143     yield downloadable; | 
|  | 144   }, | 
|  | 145 | 
|  | 146   _onExpirationChange: function(downloadable) | 
|  | 147   { | 
|  | 148     Prefs.sendstats_status.lastCheck = downloadable.lastCheck; | 
|  | 149     this._saveFilterHitPrefs(); | 
|  | 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(); | 
|  | 162   }, | 
| 39 | 163 | 
| 40   /** | 164   /** | 
| 41    * Increases the filter hit count | 165    * Increases the filter hit count | 
| 42    * @param {Filter} filter | 166    * @param {Filter} filter | 
| 43    * @param {Window} window  Window that the match originated in (required to ge
     t host) | 167    * @param {String} host | 
| 44    */ | 168    */ | 
| 45   increaseFilterHits: function(filter, wnd) | 169   increaseFilterHits: function(filter, host, thirdParty) | 
| 46   { | 170   { | 
| 47     if (!filter.text || (filter.subscriptions[0] && filter.subscriptions[0].url.
     indexOf("~user~") == 0)) | 171     let subscriptions = filter.subscriptions; | 
|  | 172     let downloadableSubscriptions = []; | 
|  | 173     for (let i = 0; i < subscriptions.length; i++) | 
|  | 174     { | 
|  | 175       if (subscriptions[i] instanceof DownloadableSubscription) | 
|  | 176         downloadableSubscriptions.push(subscriptions[i].url); | 
|  | 177     } | 
|  | 178 | 
|  | 179     if (downloadableSubscriptions.length == 0) | 
| 48       return; | 180       return; | 
| 49 | 181 | 
| 50     if (!(filter.text in this.filters)) | 182     if (!this.connection) | 
| 51       this.filters[filter.text] = {}; | 183       return; | 
| 52 | 184 | 
| 53     let filterType = filter.thirdParty ? "thirdParty" : "firstParty"; | 185     let statements = []; | 
| 54 | 186     let filterSubscriptions = JSON.stringify(downloadableSubscriptions); | 
| 55     if (!(filterType in this.filters[filter.text])) | 187     let subscriptionStatement = this.connection.createStatement("INSERT OR " + | 
| 56       this.filters[filter.text][filterType] = {}; | 188       "IGNORE INTO filtersubscriptions (subscriptions) VALUES " + | 
| 57 | 189       "(:subscriptions)"); | 
| 58     if (!("subscriptions" in this.filters[filter.text])) | 190     subscriptionStatement.params.subscriptions = filterSubscriptions; | 
| 59       this.filters[filter.text].subscriptions = []; | 191     statements.push(subscriptionStatement); | 
| 60 | 192     let filterHitStatement = this.connection.createStatement("INSERT OR " + | 
| 61     if (filter.subscriptions) | 193       "REPLACE INTO filterhits (filter, host, thirdParty, hitCount, lastHit, " + | 
| 62     { | 194       "subscriptions) VALUES (:filter, :host, :thirdParty, COALESCE ((SELECT " + | 
| 63       for (let i = 0; i < filter.subscriptions.length; i++) | 195       "hitCount  FROM filterhits WHERE filter=:filter AND host=:host AND " + | 
| 64       { | 196       "thirdParty=:thirdParty), 0) + 1, :lastHit, (SELECT id " + | 
| 65         if (this.filters[filter.text].subscriptions.indexOf(filter.subscriptions
     [i]._title) == -1) | 197       "FROM filtersubscriptions WHERE subscriptions=:subscriptions))"); | 
| 66           this.filters[filter.text].subscriptions.push(filter.subscriptions[i]._
     title); | 198     filterHitStatement.params.filter = filter.text; | 
|  | 199     filterHitStatement.params.host = host; | 
|  | 200     filterHitStatement.params.lastHit = roundToHours(filter.lastHit); | 
|  | 201     filterHitStatement.params.thirdParty = thirdParty; | 
|  | 202     filterHitStatement.params.subscriptions = filterSubscriptions; | 
|  | 203     statements.push(filterHitStatement); | 
|  | 204 | 
|  | 205     this.connection.executeAsync(statements, statements.length, | 
|  | 206     { | 
|  | 207       handleError: (error) => | 
|  | 208       { | 
|  | 209         Cu.reportError("Error updating filter hits: " + error.message); | 
|  | 210       }, | 
|  | 211       handleCompletion: (reason) => | 
|  | 212       { | 
|  | 213         if (reason != REASON_FINISHED) | 
|  | 214           Cu.reportError("Updating filter hits canceled or aborted: " + reason); | 
| 67       } | 215       } | 
| 68     } | 216     }); | 
| 69 | 217   }, | 
| 70     let wndLocation = Utils.getOriginWindow(wnd).location.href; | 218 | 
| 71     let host = Utils.unwrapURL(wndLocation).host; | 219   /** | 
| 72 | 220    * Remove all local collected filter hits data | 
| 73     if (!(host in this.filters[filter.text][filterType])) | 221    */ | 
| 74       this.filters[filter.text][filterType][host] = {hits: 1, latest: filter.las
     tHit}; |  | 
| 75     else |  | 
| 76     { |  | 
| 77       this.filters[filter.text][filterType][host].hits++; |  | 
| 78       this.filters[filter.text][filterType][host].latest = filter.lastHit; |  | 
| 79     } |  | 
| 80   }, |  | 
| 81 |  | 
| 82   resetFilterHits: function() | 222   resetFilterHits: function() | 
| 83   { | 223   { | 
| 84     this.filters = {}; | 224     if (!this.connection) | 
| 85     this.saveFilterHitsToDatabase(); | 225       return; | 
| 86   }, | 226 | 
| 87 | 227     this.connection.executeSimpleSQL("DELETE FROM filterhits"); | 
| 88   sendFilterHitsToServer: function() | 228     this.connection.executeSimpleSQL("DELETE FROM filtersubscriptions"); | 
| 89   { | 229   }, | 
| 90     let prepareData = function() | 230 | 
| 91     { | 231   _generateFilterHitsData: function(downloadable) | 
| 92       let {addonName, addonVersion, application, applicationVersion, platform, p
     latformVersion} = require("info"); | 232   { | 
| 93       return { | 233     return { | 
| 94         version: 1, | 234       then: (resolve) => | 
| 95         timeSincePush: this._lastPush, | 235       { | 
| 96         addonName: addonName, | 236         if (!this.connection) | 
| 97         addonVersion: addonVersion, | 237           return; | 
| 98         application: application, | 238 | 
| 99         applicationVersion: applicationVersion, | 239         let statement = this.connection.createStatement("SELECT filterhits.*, " 
     + | 
| 100         platform: platform, | 240         "filtersubscriptions.subscriptions FROM filterhits LEFT JOIN " + | 
| 101         platformVersion: platformVersion, | 241         "filtersubscriptions filtersubscriptions ON " + | 
| 102         filters: this.filters | 242         "filterhits.subscriptions=filtersubscriptions.id"); | 
|  | 243         statement.executeAsync( | 
|  | 244         { | 
|  | 245           handleResult: (result) => | 
|  | 246           { | 
|  | 247             let filters = Object.create(null); | 
|  | 248             for (let row = result.getNextRow(); row; | 
|  | 249                  row = result.getNextRow()) | 
|  | 250             { | 
|  | 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) => | 
|  | 271           { | 
|  | 272             Prefs.sendstats_status.lastError = Date.now(); | 
|  | 273             this._saveFilterHitPrefs(); | 
|  | 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             } | 
|  | 285           } | 
|  | 286         }); | 
| 103       } | 287       } | 
| 104     }.bind(this); | 288     } | 
| 105 | 289   }, | 
| 106     let request = new XMLHttpRequest(); |  | 
| 107     request.open("POST", this._serviceURL); |  | 
| 108     request.setRequestHeader("Content-Type", "application/json"); |  | 
| 109     request.addEventListener("load", function(event) |  | 
| 110     { |  | 
| 111       let request = event.target; |  | 
| 112       FilterHits._sending = false; |  | 
| 113       if (request.status == 200) |  | 
| 114       { |  | 
| 115         FilterHits._lastPush = new Date().getTime(); |  | 
| 116         FilterHits.resetFilterHits(); |  | 
| 117       } |  | 
| 118       else |  | 
| 119         Cu.reportError("could not send filter hit statistics to AdBlock Plus ser
     ver"); |  | 
| 120     }, false); |  | 
| 121     this._sending = true; |  | 
| 122     request.send(JSON.stringify(prepareData())); |  | 
| 123   }, |  | 
| 124 |  | 
| 125   getStorageFile: function() |  | 
| 126   { |  | 
| 127     return FileUtils.getFile("ProfD", ["adblockplus.sqlite"]); |  | 
| 128   }, |  | 
| 129 |  | 
| 130   checkCreateTable: function(connection) |  | 
| 131   { |  | 
| 132     if (!connection.tableExists("filterhits")) |  | 
| 133       connection.executeSimpleSQL("CREATE TABLE filterhits (id INTEGER PRIMARY K
     EY, filters TEXT, date INTEGER)"); |  | 
| 134   }, |  | 
| 135 |  | 
| 136   /** |  | 
| 137    * Load Filter hits from database |  | 
| 138    */ |  | 
| 139   loadFilterHitsFromDatabase: function() |  | 
| 140   { |  | 
| 141     let storageFile = this.getStorageFile(); |  | 
| 142     if (!storageFile) |  | 
| 143       return; |  | 
| 144 |  | 
| 145     let connection = Services.storage.openDatabase(storageFile); |  | 
| 146     this.checkCreateTable(connection); |  | 
| 147 |  | 
| 148     let statement = connection.createStatement("SELECT * FROM filterhits"); |  | 
| 149     if (!this._loading) |  | 
| 150     { |  | 
| 151       this._loading = true; |  | 
| 152       statement.executeAsync( |  | 
| 153       { |  | 
| 154         handleResult: function(aResultSet) |  | 
| 155         { |  | 
| 156           let row = aResultSet.getNextRow(); |  | 
| 157           if (row) |  | 
| 158           { |  | 
| 159             let filters = row.getResultByName("filters"); |  | 
| 160             let lastDate = row.getResultByName("date"); |  | 
| 161             FilterHits.filters = JSON.parse(filters); |  | 
| 162             FilterHits._lastPush = lastDate; |  | 
| 163           } |  | 
| 164           FilterHits._loading = false; |  | 
| 165         }, |  | 
| 166 |  | 
| 167         handleError: function(aError) |  | 
| 168         { |  | 
| 169           Cu.reportError(aError.message); |  | 
| 170         }, |  | 
| 171 |  | 
| 172         handleCompletion: function(aReason) |  | 
| 173         { |  | 
| 174           if (aReason != Components.interfaces.mozIStorageStatementCallback.REAS
     ON_FINISHED) |  | 
| 175           { |  | 
| 176             Cu.reportError("Loading of filter hits canceled or aborted."); |  | 
| 177             FilterHits._loading = false; |  | 
| 178           } |  | 
| 179         } |  | 
| 180       }); |  | 
| 181     } |  | 
| 182 |  | 
| 183     connection.asyncClose(); |  | 
| 184   }, |  | 
| 185 |  | 
| 186   /** |  | 
| 187    * Save Filter hits to database |  | 
| 188    */ |  | 
| 189   saveFilterHitsToDatabase: function() |  | 
| 190   { |  | 
| 191     if (!this._lastPush) |  | 
| 192       this._lastPush = new Date().getTime(); |  | 
| 193 |  | 
| 194     if (!this._sending && new Date().getTime() - this._lastPush > this._pushInte
     rval) |  | 
| 195     { |  | 
| 196       this.sendFilterHitsToServer(); |  | 
| 197       return; |  | 
| 198     } |  | 
| 199 |  | 
| 200     let storageFile = this.getStorageFile(); |  | 
| 201     if (!storageFile) |  | 
| 202       return; |  | 
| 203 |  | 
| 204     let connection = Services.storage.openDatabase(storageFile); |  | 
| 205     this.checkCreateTable(connection); |  | 
| 206 |  | 
| 207     let statement = connection.createStatement("INSERT OR REPLACE INTO filterhit
     s (id, filters, date) VALUES(0, :filters, :date)"); |  | 
| 208     statement.params.filters = JSON.stringify(this.filters); |  | 
| 209     statement.params.date = this._lastPush; |  | 
| 210     if (!this._saving) |  | 
| 211     { |  | 
| 212       this._saving = true; |  | 
| 213       statement.executeAsync( |  | 
| 214       { |  | 
| 215         handleError: function(aError) |  | 
| 216         { |  | 
| 217           Cu.reportError(aError.message); |  | 
| 218         }, |  | 
| 219 |  | 
| 220         handleCompletion: function(aReason) |  | 
| 221         { |  | 
| 222           if (aReason != Components.interfaces.mozIStorageStatementCallback.REAS
     ON_FINISHED) |  | 
| 223             Cu.reportError("Writing of filter hits canceled or aborted."); |  | 
| 224           FilterHits._saving = false; |  | 
| 225         } |  | 
| 226       }); |  | 
| 227     } |  | 
| 228 |  | 
| 229     connection.asyncClose(); |  | 
| 230   } |  | 
| 231 }; | 290 }; | 
|  | 291 | 
|  | 292 function roundToHours(timestamp) | 
|  | 293 { | 
|  | 294   return Math.round(timestamp / MILLIS_IN_HOUR) * | 
|  | 295       (MILLIS_IN_HOUR / MILLIS_IN_SECOND); | 
|  | 296 } | 
|  | 297 | 
|  | 298 FilterHits.init(); | 
| LEFT | RIGHT | 
|---|