| Index: lib/filterHits.js |
| =================================================================== |
| new file mode 100644 |
| --- /dev/null |
| +++ b/lib/filterHits.js |
| @@ -0,0 +1,298 @@ |
| +/* |
| + * 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, clearTimeout} = Cu.import("resource://gre/modules/Timer.jsm", null); |
| + |
| +let {IO} = require("io"); |
| +let {Prefs} = require("prefs"); |
| +let {Downloader, Downloadable, MILLIS_IN_SECOND, MILLIS_IN_MINUTE, |
| + MILLIS_IN_HOUR, MILLIS_IN_DAY} = require("downloader"); |
| +let {FilterNotifier} = require("filterNotifier"); |
| +let {DownloadableSubscription} = require("subscriptionClasses"); |
| + |
| +const CHECK_INTERVAL = MILLIS_IN_HOUR; |
| +const INITIAL_DELAY = MILLIS_IN_MINUTE * 2; |
| +const PUSH_INTERVAL = MILLIS_IN_DAY * 7; |
| + |
| +/** |
| + * The value of storage statement normal execution constant |
| + * @type Number |
| + */ |
| +const REASON_FINISHED = Components.interfaces.mozIStorageStatementCallback.REASON_FINISHED; |
| + |
| +/** |
| + * This class collects filter hits statistics in a SQLite database |
| + * and sends them to the server when user opt-in for that |
| + * @class |
| + */ |
| +let FilterHits = exports.FilterHits = |
| +{ |
| + /** |
| + * Used to prevent timers running in parallel |
| + * @type Number |
| + */ |
| + _prefToggleTimeout: 0, |
| + |
| + get connection() |
| + { |
| + if (!this.storageFile) |
| + return null; |
| + |
| + let connection = Services.storage.openDatabase(this.storageFile); |
| + if (!connection.tableExists("filtersubscriptions")) |
| + { |
| + connection.executeSimpleSQL("CREATE TABLE filtersubscriptions " + |
| + "(id INTEGER, subscriptions TEXT, PRIMARY KEY" + |
| + "(id), UNIQUE(subscriptions))"); |
| + } |
| + if (!connection.tableExists("filterhits")) |
| + { |
| + connection.executeSimpleSQL("CREATE TABLE filterhits (filter TEXT, " + |
| + "host TEXT, thirdParty INTEGER, hitCount INTEGER, lastHit INTEGER, " + |
| + "subscriptions INTEGER NOT NULL, PRIMARY KEY(filter, host, " + |
| + "thirdParty), FOREIGN KEY(subscriptions) REFERENCES "+ |
| + "filtersubscriptions(id))"); |
| + } |
| + Object.defineProperty(this, "connection", |
| + { |
| + value: connection |
| + }); |
| + return connection; |
| + }, |
| + |
| + /** |
| + * @return nsIFile SQLite database file with filter hits data |
| + */ |
| + get storageFile() |
| + { |
| + let file = IO.resolveFilePath(Prefs.data_directory); |
| + if (file) |
| + file.append("adblockplus.sqlite"); |
| + |
| + Object.defineProperty(this, "storageFile", |
| + { |
| + value: file |
| + }); |
| + return file; |
| + }, |
| + |
| + /** |
| + * Called on module startup. |
| + */ |
| + init: function() |
| + { |
| + if (!Prefs.sendstats_status.lastPush) |
| + { |
| + Prefs.sendstats_status.lastPush = Date.now(); |
| + this._saveFilterHitPrefs(); |
| + } |
| + Prefs.addListener(name => |
| + { |
| + if (name == "sendstats") |
| + { |
| + if (this._prefToggleTimeout) |
| + clearTimeout(this._prefToggleTimeout); |
| + this._prefToggleTimeout = setTimeout(() => |
| + { |
| + if (!Prefs.sendstats) |
| + this.resetFilterHits(); |
| + }, 1000); |
| + } |
| + }); |
| + |
| + let downloader = new Downloader(this._getDownloadables.bind(this), |
| + INITIAL_DELAY, CHECK_INTERVAL); |
| + downloader.onExpirationChange = this._onExpirationChange.bind(this); |
| + downloader.generateRequestData = this._generateFilterHitsData.bind(this); |
| + downloader.onDownloadSuccess = this._onDownloadSuccess.bind(this); |
| + downloader.validResponses.push(204); |
| + onShutdown.add(() => downloader.cancel()); |
| + }, |
| + |
| + /** |
| + * Yields a Downloadable instance for the filter hits push. |
| + */ |
| + _getDownloadables: function*() |
| + { |
| + let downloadable = new Downloadable(Prefs.sendstats_url); |
| + if (Prefs.sendstats_status.lastError) |
| + downloadable.lastError = Prefs.sendstats_status.lastError; |
| + if (Prefs.sendstats_status.lastCheck) |
| + downloadable.lastCheck = Prefs.sendstats_status.lastCheck; |
| + if (Prefs.sendstats_status.lastPush) |
| + { |
| + downloadable.softExpiration = Prefs.sendstats_status.lastPush + PUSH_INTERVAL; |
|
saroyanm
2016/04/06 15:15:45
I'm not sure regarding this, should we specify sof
|
| + downloadable.hardExpiration = Prefs.sendstats_status.lastPush + PUSH_INTERVAL; |
| + } |
| + yield downloadable; |
| + }, |
| + |
| + _onExpirationChange: function(downloadable) |
| + { |
| + Prefs.sendstats_status.lastCheck = downloadable.lastCheck; |
| + this._saveFilterHitPrefs(); |
| + }, |
| + |
| + _saveFilterHitPrefs: function() |
| + { |
| + Prefs.sendstats_status = JSON.parse(JSON.stringify(Prefs.sendstats_status)); |
| + }, |
| + |
| + _onDownloadSuccess: function(downloadable, responseText, errorCallback, redirectCallback) |
| + { |
| + Prefs.sendstats_status.lastError = 0; |
| + Prefs.sendstats_status.lastPush = Date.now(); |
| + this._saveFilterHitPrefs(); |
| + }, |
| + |
| + /** |
| + * Increases the filter hit count |
| + * @param {Filter} filter |
| + * @param {String} host |
| + */ |
| + increaseFilterHits: function(filter, host, thirdParty) |
| + { |
| + 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; |
| + |
| + if (!this.connection) |
| + return; |
| + |
| + let statements = []; |
| + let filterSubscriptions = JSON.stringify(downloadableSubscriptions); |
| + let subscriptionStatement = this.connection.createStatement("INSERT OR " + |
| + "IGNORE INTO filtersubscriptions (subscriptions) VALUES " + |
| + "(:subscriptions)"); |
| + subscriptionStatement.params.subscriptions = filterSubscriptions; |
| + statements.push(subscriptionStatement); |
| + let filterHitStatement = this.connection.createStatement("INSERT OR " + |
| + "REPLACE INTO filterhits (filter, host, thirdParty, hitCount, lastHit, " + |
| + "subscriptions) VALUES (:filter, :host, :thirdParty, COALESCE ((SELECT " + |
| + "hitCount FROM filterhits WHERE filter=:filter AND host=:host AND " + |
| + "thirdParty=:thirdParty), 0) + 1, :lastHit, (SELECT id " + |
| + "FROM filtersubscriptions WHERE subscriptions=:subscriptions))"); |
| + filterHitStatement.params.filter = filter.text; |
| + filterHitStatement.params.host = host; |
| + filterHitStatement.params.lastHit = roundToHours(filter.lastHit); |
| + filterHitStatement.params.thirdParty = thirdParty; |
| + filterHitStatement.params.subscriptions = filterSubscriptions; |
| + statements.push(filterHitStatement); |
| + |
| + this.connection.executeAsync(statements, statements.length, |
| + { |
| + handleError: (error) => |
| + { |
| + Cu.reportError("Error updating filter hits: " + error.message); |
| + }, |
| + handleCompletion: (reason) => |
| + { |
| + if (reason != REASON_FINISHED) |
| + Cu.reportError("Updating filter hits canceled or aborted: " + reason); |
| + } |
| + }); |
| + }, |
| + |
| + /** |
| + * Remove all local collected filter hits data |
| + */ |
| + resetFilterHits: function() |
| + { |
| + if (!this.connection) |
| + return; |
| + |
| + this.connection.executeSimpleSQL("DELETE FROM filterhits"); |
| + this.connection.executeSimpleSQL("DELETE FROM filtersubscriptions"); |
| + }, |
| + |
| + _generateFilterHitsData: function(downloadable) |
| + { |
| + return { |
| + then: (resolve) => |
| + { |
| + if (!this.connection) |
| + return; |
| + |
| + let statement = this.connection.createStatement("SELECT filterhits.*, " + |
| + "filtersubscriptions.subscriptions FROM filterhits LEFT JOIN " + |
| + "filtersubscriptions filtersubscriptions ON " + |
| + "filterhits.subscriptions=filtersubscriptions.id"); |
| + statement.executeAsync( |
| + { |
| + handleResult: (result) => |
| + { |
| + let filters = Object.create(null); |
| + for (let row = result.getNextRow(); row; |
| + row = result.getNextRow()) |
| + { |
| + let filterText = row.getResultByName("filter"); |
| + let host = row.getResultByName("host"); |
| + let matchType = (row.getResultByName("thirdParty") ? |
| + "thirdParty" : "firstParty"); |
| + 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]; |
| + filter.subscriptions = subscriptions; |
| + filter[matchType] = Object.create(null); |
| + filter[matchType][host] = {"hits": hitCount, latest: lastHit}; |
| + } |
| + resolve({timeSincePush: roundToHours(Date.now() - |
| + Prefs.sendstats_status.lastPush), "filters": filters}); |
| + }, |
| + handleError: (error) => |
| + { |
| + Prefs.sendstats_status.lastError = Date.now(); |
| + this._saveFilterHitPrefs(); |
| + Cu.reportError("Error loading filter hits:" + error.message); |
| + }, |
| + handleCompletion: (reason) => |
| + { |
| + if (reason != REASON_FINISHED) |
| + { |
| + Prefs.sendstats_status.lastError = Date.now(); |
| + this._saveFilterHitPrefs(); |
| + Cu.reportError("Loading of filter hits canceled or aborted: " + |
| + reason); |
| + } |
| + } |
| + }); |
| + } |
| + } |
| + }, |
| +}; |
| + |
| +function roundToHours(timestamp) |
| +{ |
| + return Math.round(timestamp / MILLIS_IN_HOUR) * |
| + (MILLIS_IN_HOUR / MILLIS_IN_SECOND); |
| +} |
| + |
| +FilterHits.init(); |