Index: lib/filterHits.js |
=================================================================== |
new file mode 100644 |
--- /dev/null |
+++ b/lib/filterHits.js |
@@ -0,0 +1,263 @@ |
+/* |
+ * This file is part of Adblock Plus <https://adblockplus.org/>, |
+ * Copyright (C) 2006-2016 Eyeo GmbH |
+ * |
+ * Adblock Plus is free software: you can redistribute it and/or modify |
+ * it under the terms of the GNU General Public License version 3 as |
+ * published by the Free Software Foundation. |
+ * |
+ * Adblock Plus is distributed in the hope that it will be useful, |
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of |
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
+ * GNU General Public License for more details. |
+ * |
+ * You should have received a copy of the GNU General Public License |
+ * along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. |
+ */ |
+ |
+let {Services} = Cu.import("resource://gre/modules/Services.jsm", null); |
+let {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm", null); |
+let {setTimeout} = Cu.import("resource://gre/modules/Timer.jsm", null); |
+ |
+let {Prefs} = require("prefs"); |
+let {Utils} = require("utils"); |
+let {MILLIS_IN_DAY} = require("downloader"); |
+let {FilterNotifier} = require("filterNotifier"); |
+let {DownloadableSubscription} = require("subscriptionClasses"); |
+ |
+/** |
+ * This class collect filter hits statistics in SQLite database |
+ * and send them to the server when user opt-in for that |
Wladimir Palant
2016/02/29 14:35:55
Nit: collect => collects, in SQLite => in an SQLit
saroyanm
2016/03/18 18:23:13
Done.
|
+ * @class |
+ */ |
+let FilterHits = exports.FilterHits = |
+{ |
+ /** |
+ * Indicates the timeframe between pushes |
+ * @type Number |
+ */ |
+ _pushInterval: MILLIS_IN_DAY * 7, |
Wladimir Palant
2016/02/29 14:35:50
That's a constant, it really shouldn't be declared
saroyanm
2016/03/18 18:23:30
Done.
|
+ |
+ /** |
+ * The value of storage statement normal execution constant |
+ * @type Number |
+ */ |
+ _finished: Components.interfaces.mozIStorageStatementCallback.REASON_FINISHED, |
Wladimir Palant
2016/02/29 14:35:48
Same here, constants shouldn't be properties.
saroyanm
2016/03/18 18:23:23
Done.
|
+ |
+ /** |
+ * Indicates whether the data is being sent to the server |
+ * @type Boolean |
+ */ |
+ _sending: false, |
+ |
+ /** |
+ * Called on module startup. |
+ */ |
+ init: function() |
+ { |
+ Prefs.addListener(function(name) |
+ { |
+ if (name == "sendstats") |
+ { |
+ setTimeout(function() |
Wladimir Palant
2016/02/29 14:35:57
Nit: Creating a number of timers running in parall
saroyanm
2016/03/18 18:23:12
Done.
|
+ { |
+ if (!Prefs[name]) |
Wladimir Palant
2016/02/29 14:35:56
Here as well, just use Prefs.sendstats please - it
saroyanm
2016/03/18 18:23:21
Done.
|
+ this.resetFilterHits(); |
+ }.bind(this), 1000); |
+ } |
+ }.bind(this)); |
Wladimir Palant
2016/02/29 14:35:53
bind(this) shouldn't be necessary in most cases, j
saroyanm
2016/03/18 18:23:31
Done.
|
+ |
+ let now = new Date().getTime(); |
+ if (now && (now - Prefs.sendstats_last_push > this._pushInterval)) |
+ this.sendFilterHitsToServer(); |
Wladimir Palant
2016/02/29 14:35:55
Please use Date.now(). Also, it's a safe bet that
saroyanm
2016/03/18 18:23:16
I'm not sure if it's a good idea to change the Dow
|
+ }, |
+ |
+ /** |
+ * Increases the filter hit count |
+ * @param {Filter} filter |
+ * @param {String} host |
+ */ |
+ increaseFilterHits: function(filter, host) |
+ { |
+ let subscriptions = filter.subscriptions; |
+ let downloadableSubscriptions = []; |
+ for (let i = 0; i < subscriptions.length; i++) |
+ { |
+ if (subscriptions[i] instanceof DownloadableSubscription) |
+ downloadableSubscriptions.push(subscriptions[i].url); |
+ } |
+ |
+ if (downloadableSubscriptions.length == 0) |
+ return; |
+ |
+ let storageFile = this.getStorageFile(); |
+ if (!storageFile) |
+ return; |
+ |
+ let connection = Services.storage.openDatabase(storageFile); |
+ this.checkCreateTable(connection); |
Wladimir Palant
2016/02/29 14:35:51
Should we be doing this on every filter hit? Initi
saroyanm
2016/03/18 18:23:24
Good point, I've cached connection opening, but st
|
+ |
+ let statementText = "INSERT OR REPLACE INTO filterhits " + |
+ "(filter, host, firstParty, hitCount, lastHit, subscriptions) VALUES " + |
+ "(:filter, :host, :firstParty, coalesce ((SELECT hitCount " + |
+ "FROM filterhits WHERE filter=:filter AND host=:host AND " + |
+ "firstParty=:firstParty), 0) + 1, :lastHit, :subscriptions)"; |
+ |
+ let statement = connection.createStatement(statementText); |
+ statement.params.filter = filter.text; |
+ statement.params.host = host; |
+ statement.params.lastHit = filter.lastHit; |
+ statement.params.firstParty = filter.thirdParty != "thirdParty"; |
Wladimir Palant
2016/02/29 14:35:52
This is a filter flag you are checking here, it on
saroyanm
2016/03/18 18:23:27
Done.
|
+ statement.params.subscriptions = JSON.stringify(downloadableSubscriptions); |
+ |
+ statement.executeAsync( |
+ { |
+ handleError: function(aError) |
Wladimir Palant
2016/02/29 14:35:45
Nit: we don't use Hungarian notation for parameter
saroyanm
2016/03/18 18:23:14
Done.
|
+ { |
+ Cu.reportError(aError.message); |
+ }, |
+ |
+ handleCompletion: function(aReason) |
+ { |
+ if (aReason != this._finished) |
+ Cu.reportError("Writing of filter hits canceled or aborted."); |
Wladimir Palant
2016/02/29 14:35:58
Maybe add aReason to the text, so that we can look
saroyanm
2016/03/18 18:23:28
Done.
|
+ }.bind(this) |
Wladimir Palant
2016/02/29 14:35:49
Please get rid of bind(this) here as well.
saroyanm
2016/03/18 18:23:22
Done.
|
+ }); |
+ |
+ connection.asyncClose(); |
+ }, |
+ |
+ /** |
+ * Remove all local collected filter hits data |
+ */ |
+ resetFilterHits: function() |
+ { |
+ let storageFile = this.getStorageFile(); |
+ if (!storageFile) |
+ return; |
+ |
+ let connection = Services.storage.openDatabase(storageFile); |
+ connection.executeSimpleSQL("DROP TABLE IF EXISTS filterhits"); |
Wladimir Palant
2016/02/29 14:35:56
Probably better to leave the database initialized
saroyanm
2016/03/18 18:23:29
Done.
|
+ }, |
+ |
+ /** |
+ * Send local stored filter hit data to the |
+ * service specified in Prefs.sendstats_url |
+ */ |
+ sendFilterHitsToServer: function() |
+ { |
+ if (!Prefs.sendstats) |
+ return; |
+ |
+ let storageFile = this.getStorageFile(); |
+ if (!storageFile) |
+ return; |
+ |
+ let connection = Services.storage.openDatabase(storageFile); |
+ this.checkCreateTable(connection); |
+ |
+ let statement = connection.createStatement("SELECT * FROM filterhits"); |
+ statement.executeAsync( |
+ { |
+ handleResult: function(aResultSet) |
+ { |
+ let filters = Object.create(null); |
+ for (let row = aResultSet.getNextRow(); row; |
+ row = aResultSet.getNextRow()) |
+ { |
+ |
+ let filterText = row.getResultByName("filter"); |
+ let host = row.getResultByName("host"); |
+ let filterType = (row.getResultByName("firstParty") == 1 ? |
Wladimir Palant
2016/02/29 14:35:52
a) Not filterType, matchType.
b) Don't compare to
saroyanm
2016/03/18 18:23:22
Done.
|
+ "firstParty" : "thirdParty"); |
+ let hitCount = row.getResultByName("hitCount"); |
+ let lastHit = row.getResultByName("lastHit"); |
+ let subscriptions = row.getResultByName("subscriptions"); |
+ |
+ if (!(filterText in filters)) |
+ filters[filterText] = Object.create(null); |
+ |
+ let filter = filters[filterText]; |
+ |
+ if (!filter[subscriptions]) |
+ filter[subscriptions] = subscriptions; |
Wladimir Palant
2016/02/29 14:35:53
You probably meant filter.subscriptions here.
In
saroyanm
2016/03/18 18:23:26
Done, but not sure if current implementation is wh
|
+ |
+ if (!(filterType in filter)) |
+ filter[filterType] = Object.create(null); |
+ |
+ if (!(host in filter[filterType])) |
Wladimir Palant
2016/02/29 14:35:50
This check is pointless, filter/type/host combinat
saroyanm
2016/03/18 18:23:16
Done.
|
+ filter[filterType][host] = {"hits": hitCount, latest: lastHit}; |
+ } |
+ |
+ let request = new XMLHttpRequest(); |
+ request.open("POST", Prefs.sendstats_url); |
+ request.setRequestHeader("Content-Type", "application/json"); |
+ request.addEventListener("load", function(event) |
Wladimir Palant
2016/02/29 14:35:58
You should handle "error" event as well, and be it
saroyanm
2016/03/18 18:23:27
Done.
|
+ { |
+ FilterHits._sending = false; |
Wladimir Palant
2016/02/29 14:35:57
Why are you setting this member? It isn't being us
saroyanm
2016/03/18 18:23:15
Done.
|
+ if (request.status >= 200 && request.status < 300) |
+ { |
+ Prefs.sendstats_last_push = new Date().getTime(); |
+ FilterHits.resetFilterHits(); |
+ } |
+ else |
+ { |
+ Cu.reportError("Couldn't send filter hit statistics to server."); |
Wladimir Palant
2016/02/29 14:35:54
I think that this should be more precise - server
saroyanm
2016/03/18 18:23:24
Done.
|
+ } |
+ }, false); |
+ |
+ let {addonName, addonVersion, application, |
+ applicationVersion, platform, platformVersion} = require("info"); |
+ let data = { |
+ version: 1, |
+ timeSincePush: Prefs.sendstats_last_push, |
Wladimir Palant
2016/02/29 14:35:54
That's the time of last push, not the time *since*
saroyanm
2016/03/18 18:23:30
Done, but I can't understand why we need to use th
|
+ addonName: addonName, |
+ addonVersion: addonVersion, |
+ application: application, |
+ applicationVersion: applicationVersion, |
+ platform: platform, |
+ platformVersion: platformVersion, |
+ filters: filters |
+ }; |
+ |
+ this._sending = true; |
+ request.send(JSON.stringify(data)); |
+ }.bind(this), |
+ handleError: function(aError) |
+ { |
+ Cu.reportError(aError.message); |
+ }, |
+ handleCompletion: function(aReason) |
+ { |
+ if (aReason != this._finished) |
+ Cu.reportError("Loading of filter hits canceled or aborted."); |
+ }.bind(this) |
+ }); |
+ connection.asyncClose(); |
+ }, |
+ |
+ /** |
+ * @return nsIFile SQLite database file with filter hits data |
+ */ |
+ getStorageFile: function() |
+ { |
+ return FileUtils.getFile("ProfD", ["adblockplus.sqlite"]); |
Wladimir Palant
2016/02/29 14:35:51
Please don't create files all over the user's prof
saroyanm
2016/03/18 18:23:14
Done.
|
+ }, |
+ |
+ /** |
+ * Creates table in local SQLite database for storing |
+ * filter hit data if the does not exists yet |
+ */ |
+ checkCreateTable: function(connection) |
+ { |
+ if (!connection.tableExists("filterhits")) |
+ { |
+ connection.executeSimpleSQL("CREATE TABLE filterhits (filter TEXT, " + |
+ "host TEXT, firstParty INTEGER, hitCount INTEGER, lastHit INTEGER, " + |
Wladimir Palant
2016/02/29 14:35:44
We always have thirdParty as a flag, please use it
saroyanm
2016/03/18 18:23:25
Done.
|
+ "subscriptions TEXT, PRIMARY KEY(filter, host, firstParty))"); |
+ Prefs.sendstats_last_push = new Date().getTime(); |
+ } |
+ }, |
+}; |
+ |
+FilterHits.init(); |