summaryrefslogtreecommitdiffstats
path: root/toolkit/components/url-classifier/UrlClassifierListManager.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/url-classifier/UrlClassifierListManager.jsm')
-rw-r--r--toolkit/components/url-classifier/UrlClassifierListManager.jsm835
1 files changed, 835 insertions, 0 deletions
diff --git a/toolkit/components/url-classifier/UrlClassifierListManager.jsm b/toolkit/components/url-classifier/UrlClassifierListManager.jsm
new file mode 100644
index 0000000000..0a8bd530f4
--- /dev/null
+++ b/toolkit/components/url-classifier/UrlClassifierListManager.jsm
@@ -0,0 +1,835 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+// This is the only implementation of nsIUrlListManager.
+// A class that manages lists, namely exception and block lists for
+// phishing or malware protection. The ListManager knows how to fetch,
+// update, and store lists.
+//
+// There is a single listmanager for the whole application.
+//
+// TODO more comprehensive update tests, for example add unittest check
+// that the listmanagers tables are properly written on updates
+
+// Lower and upper limits on the server-provided polling frequency
+const minDelayMs = 5 * 60 * 1000;
+const maxDelayMs = 24 * 60 * 60 * 1000;
+const defaultUpdateIntervalMs = 30 * 60 * 1000;
+const PREF_DEBUG_ENABLED = "browser.safebrowsing.debug";
+const PREF_TEST_NOTIFICATIONS =
+ "browser.safebrowsing.test-notifications.enabled";
+
+let loggingEnabled = false;
+
+// Variables imported from library.
+let BindToObject, RequestBackoffV4;
+
+// Log only if browser.safebrowsing.debug is true
+function log(...stuff) {
+ if (!loggingEnabled) {
+ return;
+ }
+
+ var d = new Date();
+ let msg = "listmanager: " + d.toTimeString() + ": " + stuff.join(" ");
+ msg = Services.urlFormatter.trimSensitiveURLs(msg);
+ Services.console.logStringMessage(msg);
+ dump(msg + "\n");
+}
+
+/**
+ * A ListManager keeps track of exception and block lists and knows
+ * how to update them.
+ *
+ * @constructor
+ */
+function PROT_ListManager() {
+ loggingEnabled = Services.prefs.getBoolPref(PREF_DEBUG_ENABLED);
+
+ log("Initializing list manager");
+ this.updateInterval = defaultUpdateIntervalMs;
+
+ // A map of tableNames to objects of type
+ // { updateUrl: <updateUrl>, gethashUrl: <gethashUrl> }
+ this.tablesData = {};
+ // A map of updateUrls to maps of tables requiring updates, e.g.
+ // { safebrowsing-update-url: { goog-phish-shavar: true,
+ // goog-malware-shavar: true }
+ this.needsUpdate_ = {};
+
+ // A map of updateUrls to single-use nsITimer. An entry exists if and only if
+ // there is at least one table with updates enabled for that url. nsITimers
+ // are reset when enabling/disabling updates or on update callbacks (update
+ // success, update failure, download error).
+ this.updateCheckers_ = {};
+ this.requestBackoffs_ = {};
+
+ // This is only used by testcases to ensure SafeBrowsing.jsm is inited
+ this.registered = false;
+
+ this.dbService_ = Cc["@mozilla.org/url-classifier/dbservice;1"].getService(
+ Ci.nsIUrlClassifierDBService
+ );
+
+ Services.obs.addObserver(this, "quit-application");
+ Services.prefs.addObserver(PREF_DEBUG_ENABLED, this);
+}
+
+/**
+ * Register a new table table
+ * @param tableName - the name of the table
+ * @param updateUrl - the url for updating the table
+ * @param gethashUrl - the url for fetching hash completions
+ * @returns true if the table could be created; false otherwise
+ */
+PROT_ListManager.prototype.registerTable = function(
+ tableName,
+ providerName,
+ updateUrl,
+ gethashUrl
+) {
+ this.registered = true;
+
+ this.tablesData[tableName] = {};
+ if (!updateUrl) {
+ log("Can't register table " + tableName + " without updateUrl");
+ return false;
+ }
+ log("registering " + tableName + " with " + updateUrl);
+ this.tablesData[tableName].updateUrl = updateUrl;
+ this.tablesData[tableName].gethashUrl = gethashUrl;
+ this.tablesData[tableName].provider = providerName;
+
+ // Keep track of all of our update URLs.
+ if (!this.needsUpdate_[updateUrl]) {
+ this.needsUpdate_[updateUrl] = {};
+
+ // Using the V4 backoff algorithm for both V2 and V4. See bug 1273398.
+ this.requestBackoffs_[updateUrl] = new RequestBackoffV4(
+ 4 /* num requests */,
+ 60 * 60 * 1000 /* request time, 60 min */,
+ providerName /* used by testcase */
+ );
+ }
+ this.needsUpdate_[updateUrl][tableName] = false;
+
+ return true;
+};
+
+/**
+ * Unregister a table table from list
+ */
+PROT_ListManager.prototype.unregisterTable = function(tableName) {
+ log("unregistering " + tableName);
+ var table = this.tablesData[tableName];
+ if (table) {
+ if (
+ !this.updatesNeeded_(table.updateUrl) &&
+ this.updateCheckers_[table.updateUrl]
+ ) {
+ this.updateCheckers_[table.updateUrl].cancel();
+ this.updateCheckers_[table.updateUrl] = null;
+ }
+ delete this.needsUpdate_[table.updateUrl][tableName];
+ }
+ delete this.tablesData[tableName];
+};
+
+/**
+ * Delete all of our data tables which seem to leak otherwise.
+ * Remove observers
+ */
+PROT_ListManager.prototype.shutdown_ = function() {
+ this.stopUpdateCheckers();
+ for (var name in this.tablesData) {
+ delete this.tablesData[name];
+ }
+ Services.obs.removeObserver(this, "quit-application");
+ Services.prefs.removeObserver(PREF_DEBUG_ENABLED, this);
+};
+
+/**
+ * xpcom-shutdown callback
+ */
+PROT_ListManager.prototype.observe = function(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "quit-application":
+ this.shutdown_();
+ break;
+ case "nsPref:changed":
+ if (aData == PREF_DEBUG_ENABLED) {
+ loggingEnabled = Services.prefs.getBoolPref(PREF_DEBUG_ENABLED);
+ }
+ break;
+ }
+};
+
+PROT_ListManager.prototype.getGethashUrl = function(tableName) {
+ if (this.tablesData[tableName] && this.tablesData[tableName].gethashUrl) {
+ return this.tablesData[tableName].gethashUrl;
+ }
+ return "";
+};
+
+PROT_ListManager.prototype.getUpdateUrl = function(tableName) {
+ if (this.tablesData[tableName] && this.tablesData[tableName].updateUrl) {
+ return this.tablesData[tableName].updateUrl;
+ }
+ return "";
+};
+
+/**
+ * Enable updates for a single table.
+ */
+PROT_ListManager.prototype.enableUpdate = function(tableName) {
+ var table = this.tablesData[tableName];
+ if (table) {
+ log("Enabling table updates for " + tableName);
+ this.needsUpdate_[table.updateUrl][tableName] = true;
+ }
+};
+
+PROT_ListManager.prototype.isRegistered = function() {
+ return this.registered;
+};
+
+/**
+ * Returns true if any table associated with the updateUrl requires updates.
+ * @param updateUrl - the updateUrl
+ */
+PROT_ListManager.prototype.updatesNeeded_ = function(updateUrl) {
+ let updatesNeeded = false;
+ for (var tableName in this.needsUpdate_[updateUrl]) {
+ if (this.needsUpdate_[updateUrl][tableName]) {
+ updatesNeeded = true;
+ }
+ }
+ return updatesNeeded;
+};
+
+/**
+ * Disable updates for all tables.
+ */
+PROT_ListManager.prototype.disableAllUpdates = function() {
+ for (const tableName of Object.keys(this.tablesData)) {
+ this.disableUpdate(tableName);
+ }
+};
+
+/**
+ * Disables updates for a single table. Avoid this internal function
+ * and use disableAllUpdates() instead.
+ */
+PROT_ListManager.prototype.disableUpdate = function(tableName) {
+ var table = this.tablesData[tableName];
+ if (table) {
+ log("Disabling table updates for " + tableName);
+ this.needsUpdate_[table.updateUrl][tableName] = false;
+ if (
+ !this.updatesNeeded_(table.updateUrl) &&
+ this.updateCheckers_[table.updateUrl]
+ ) {
+ this.updateCheckers_[table.updateUrl].cancel();
+ this.updateCheckers_[table.updateUrl] = null;
+ }
+ }
+};
+
+/**
+ * Determine if we have some tables that need updating.
+ */
+PROT_ListManager.prototype.requireTableUpdates = function() {
+ for (var name in this.tablesData) {
+ // Tables that need updating even if other tables don't require it
+ if (this.needsUpdate_[this.tablesData[name].updateUrl][name]) {
+ return true;
+ }
+ }
+
+ return false;
+};
+
+/**
+ * Set timer to check update after delay
+ */
+PROT_ListManager.prototype.setUpdateCheckTimer = function(updateUrl, delay) {
+ this.updateCheckers_[updateUrl] = Cc["@mozilla.org/timer;1"].createInstance(
+ Ci.nsITimer
+ );
+ this.updateCheckers_[updateUrl].initWithCallback(
+ () => {
+ this.updateCheckers_[updateUrl] = null;
+ if (updateUrl && !this.checkForUpdates(updateUrl)) {
+ // Make another attempt later.
+ this.setUpdateCheckTimer(updateUrl, this.updateInterval);
+ }
+ },
+ delay,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+};
+/**
+ * Acts as a nsIUrlClassifierCallback for getTables.
+ */
+PROT_ListManager.prototype.kickoffUpdate_ = function() {
+ this.startingUpdate_ = false;
+ var initialUpdateDelay = 3000;
+ // Add a fuzz of 0-1 minutes for both v2 and v4 according to Bug 1305478.
+ initialUpdateDelay += Math.floor(Math.random() * (1 * 60 * 1000));
+
+ // If the user has never downloaded tables, do the check now.
+ log("needsUpdate: " + JSON.stringify(this.needsUpdate_, undefined, 2));
+ for (var updateUrl in this.needsUpdate_) {
+ // If we haven't already kicked off updates for this updateUrl, set a
+ // non-repeating timer for it. The timer delay will be reset either on
+ // updateSuccess to this.updateInterval, or backed off on downloadError.
+ // Don't set the updateChecker unless at least one table has updates
+ // enabled.
+ if (this.updatesNeeded_(updateUrl) && !this.updateCheckers_[updateUrl]) {
+ let provider = null;
+ Object.keys(this.tablesData).forEach(function(table) {
+ if (this.tablesData[table].updateUrl === updateUrl) {
+ let newProvider = this.tablesData[table].provider;
+ if (provider) {
+ if (newProvider !== provider) {
+ log(
+ "Multiple tables for the same updateURL have a different provider?!"
+ );
+ }
+ } else {
+ provider = newProvider;
+ }
+ }
+ }, this);
+ log(
+ "Initializing update checker for " +
+ updateUrl +
+ " provided by " +
+ provider
+ );
+
+ // Use the initialUpdateDelay + fuzz unless we had previous updates
+ // and the server told us when to try again.
+ let updateDelay = initialUpdateDelay;
+ let nextUpdatePref =
+ "browser.safebrowsing.provider." + provider + ".nextupdatetime";
+ let nextUpdate = Services.prefs.getCharPref(nextUpdatePref, "");
+
+ if (nextUpdate) {
+ updateDelay = Math.min(
+ maxDelayMs,
+ Math.max(0, nextUpdate - Date.now())
+ );
+ log("Next update at " + nextUpdate);
+ }
+ log("Next update " + Math.round(updateDelay / 60000) + "min from now");
+
+ this.setUpdateCheckTimer(updateUrl, updateDelay);
+ } else {
+ log("No updates needed or already initialized for " + updateUrl);
+ }
+ }
+};
+
+PROT_ListManager.prototype.stopUpdateCheckers = function() {
+ log("Stopping updates");
+ for (var updateUrl in this.updateCheckers_) {
+ if (this.updateCheckers_[updateUrl]) {
+ this.updateCheckers_[updateUrl].cancel();
+ this.updateCheckers_[updateUrl] = null;
+ }
+ }
+};
+
+/**
+ * Determine if we have any tables that require updating. Different
+ * Wardens may call us with new tables that need to be updated.
+ */
+PROT_ListManager.prototype.maybeToggleUpdateChecking = function() {
+ // We update tables if we have some tables that want updates. If there
+ // are no tables that want to be updated - we dont need to check anything.
+ if (this.requireTableUpdates()) {
+ log("Starting managing lists");
+
+ // Get the list of existing tables from the DBService before making any
+ // update requests.
+ if (!this.startingUpdate_) {
+ this.startingUpdate_ = true;
+ // check the current state of tables in the database
+ this.kickoffUpdate_();
+ }
+ } else {
+ log("Stopping managing lists (if currently active)");
+ this.stopUpdateCheckers(); // Cancel pending updates
+ }
+};
+
+/**
+ * Force updates for the given tables. This API may trigger more than one update
+ * if the table lists provided belong to multiple updateurl (multiple provider).
+ * Return false when any update is fail due to back-off algorithm.
+ */
+PROT_ListManager.prototype.forceUpdates = function(tables) {
+ log("forceUpdates with " + tables);
+ if (!tables) {
+ return false;
+ }
+
+ let updateUrls = new Set();
+ tables.split(",").forEach(table => {
+ if (this.tablesData[table]) {
+ updateUrls.add(this.tablesData[table].updateUrl);
+ }
+ });
+
+ let ret = true;
+
+ updateUrls.forEach(url => {
+ // Cancel current update timer for the url because we are forcing an update.
+ if (this.updateCheckers_[url]) {
+ this.updateCheckers_[url].cancel();
+ this.updateCheckers_[url] = null;
+ }
+
+ // Trigger an update for the given url.
+ if (!this.checkForUpdates(url, true)) {
+ ret = false;
+ }
+ });
+
+ return ret;
+};
+
+/**
+ * Updates our internal tables from the update server
+ *
+ * @param updateUrl: request updates for tables associated with that url, or
+ * for all tables if the url is empty.
+ * @param manual: the update is triggered manually
+ */
+PROT_ListManager.prototype.checkForUpdates = function(
+ updateUrl,
+ manual = false
+) {
+ log("checkForUpdates with " + updateUrl);
+ // See if we've triggered the request backoff logic.
+ if (!updateUrl) {
+ return false;
+ }
+
+ // Disable SafeBrowsing updates in Safe Mode, but still allow manually
+ // triggering an update for debugging.
+ if (Services.appinfo.inSafeMode && !manual) {
+ log("update is disabled in Safe Mode");
+ return false;
+ }
+
+ if (lazy.enableTestNotifications) {
+ Services.obs.notifyObservers(
+ null,
+ "safebrowsing-update-attempt",
+ updateUrl
+ );
+ }
+
+ if (
+ !this.requestBackoffs_[updateUrl] ||
+ !this.requestBackoffs_[updateUrl].canMakeRequest()
+ ) {
+ log("Can't make update request");
+ return false;
+ }
+ // Grab the current state of the tables from the database
+ this.dbService_.getTables(
+ BindToObject(this.makeUpdateRequest_, this, updateUrl)
+ );
+ return true;
+};
+
+/**
+ * Method that fires the actual HTTP update request.
+ * First we reset any tables that have disappeared.
+ * @param tableData List of table data already in the database, in the form
+ * tablename;<chunk ranges>\n
+ */
+PROT_ListManager.prototype.makeUpdateRequest_ = function(updateUrl, tableData) {
+ log("this.tablesData: " + JSON.stringify(this.tablesData, undefined, 2));
+ log("existing chunks: " + tableData + "\n");
+ // Disallow blank updateUrls
+ if (!updateUrl) {
+ return;
+ }
+ // An object of the form
+ // { tableList: comma-separated list of tables to request,
+ // tableNames: map of tables that need updating,
+ // request: list of tables and existing chunk ranges from tableData
+ // }
+ var streamerMap = {
+ tableList: null,
+ tableNames: {},
+ requestPayload: "",
+ isPostRequest: true,
+ };
+
+ let useProtobuf = false;
+ let onceThru = false;
+ for (var tableName in this.tablesData) {
+ // Skip tables not matching this update url
+ if (this.tablesData[tableName].updateUrl != updateUrl) {
+ continue;
+ }
+
+ // Check if |updateURL| is for 'proto'. (only v4 uses protobuf for now.)
+ // We use the table name 'goog-*-proto' and an additional provider "google4"
+ // to describe the v4 settings.
+ let isCurTableProto = tableName.endsWith("-proto");
+ if (!onceThru) {
+ useProtobuf = isCurTableProto;
+ onceThru = true;
+ } else if (useProtobuf !== isCurTableProto) {
+ log(
+ 'ERROR: Cannot mix "proto" tables with other types ' +
+ "within the same provider."
+ );
+ }
+
+ if (this.needsUpdate_[this.tablesData[tableName].updateUrl][tableName]) {
+ streamerMap.tableNames[tableName] = true;
+ }
+ if (!streamerMap.tableList) {
+ streamerMap.tableList = tableName;
+ } else {
+ streamerMap.tableList += "," + tableName;
+ }
+ }
+
+ if (useProtobuf) {
+ let tableArray = [];
+ Object.keys(streamerMap.tableNames).forEach(aTableName => {
+ if (streamerMap.tableNames[aTableName]) {
+ tableArray.push(aTableName);
+ }
+ });
+
+ // Build the <tablename, stateBase64> mapping.
+ let tableState = {};
+ tableData.split("\n").forEach(line => {
+ let p = line.indexOf(";");
+ if (-1 === p) {
+ return;
+ }
+ let tableName = line.substring(0, p);
+ if (tableName in streamerMap.tableNames) {
+ let metadata = line.substring(p + 1).split(":");
+ let stateBase64 = metadata[0];
+ log(tableName + " ==> " + stateBase64);
+ tableState[tableName] = stateBase64;
+ }
+ });
+
+ // The state is a byte stream which server told us from the
+ // last table update. The state would be used to do the partial
+ // update and the empty string means the table has
+ // never been downloaded. See Bug 1287058 for supporting
+ // partial update.
+ let stateArray = [];
+ tableArray.forEach(listName => {
+ stateArray.push(tableState[listName] || "");
+ });
+
+ log("stateArray: " + stateArray);
+
+ let urlUtils = Cc["@mozilla.org/url-classifier/utils;1"].getService(
+ Ci.nsIUrlClassifierUtils
+ );
+
+ streamerMap.requestPayload = urlUtils.makeUpdateRequestV4(
+ tableArray,
+ stateArray
+ );
+ streamerMap.isPostRequest = false;
+ } else {
+ // Build the request. For each table already in the database, include the
+ // chunk data from the database
+ var lines = tableData.split("\n");
+ for (var i = 0; i < lines.length; i++) {
+ var fields = lines[i].split(";");
+ var name = fields[0];
+ if (streamerMap.tableNames[name]) {
+ streamerMap.requestPayload += lines[i] + "\n";
+ delete streamerMap.tableNames[name];
+ }
+ }
+ // For each requested table that didn't have chunk data in the database,
+ // request it fresh
+ for (let tableName in streamerMap.tableNames) {
+ streamerMap.requestPayload += tableName + ";\n";
+ }
+
+ streamerMap.isPostRequest = true;
+ }
+
+ log("update request: " + JSON.stringify(streamerMap, undefined, 2) + "\n");
+
+ // Don't send an empty request.
+ if (streamerMap.requestPayload.length) {
+ this.makeUpdateRequestForEntry_(
+ updateUrl,
+ streamerMap.tableList,
+ streamerMap.requestPayload,
+ streamerMap.isPostRequest
+ );
+ } else {
+ // We were disabled between kicking off getTables and now.
+ log("Not sending empty request");
+ }
+};
+
+PROT_ListManager.prototype.makeUpdateRequestForEntry_ = function(
+ updateUrl,
+ tableList,
+ requestPayload,
+ isPostRequest
+) {
+ log(
+ "makeUpdateRequestForEntry_: requestPayload " +
+ requestPayload +
+ " update: " +
+ updateUrl +
+ " tablelist: " +
+ tableList +
+ "\n"
+ );
+ var streamer = Cc["@mozilla.org/url-classifier/streamupdater;1"].getService(
+ Ci.nsIUrlClassifierStreamUpdater
+ );
+
+ this.requestBackoffs_[updateUrl].noteRequest();
+
+ if (
+ !streamer.downloadUpdates(
+ tableList,
+ requestPayload,
+ isPostRequest,
+ updateUrl,
+ BindToObject(this.updateSuccess_, this, tableList, updateUrl),
+ BindToObject(this.updateError_, this, tableList, updateUrl),
+ BindToObject(this.downloadError_, this, tableList, updateUrl)
+ )
+ ) {
+ // Our alarm gets reset in one of the 3 callbacks.
+ log("pending update, queued request until later");
+ } else {
+ let table = Object.keys(this.tablesData).find(key => {
+ return this.tablesData[key].updateUrl === updateUrl;
+ });
+ let provider = this.tablesData[table].provider;
+ Services.obs.notifyObservers(null, "safebrowsing-update-begin", provider);
+ }
+};
+
+/**
+ * Callback function if the update request succeeded.
+ * @param waitForUpdate String The number of seconds that the client should
+ * wait before requesting again.
+ */
+PROT_ListManager.prototype.updateSuccess_ = function(
+ tableList,
+ updateUrl,
+ waitForUpdateSec
+) {
+ log(
+ "update success for " +
+ tableList +
+ " from " +
+ updateUrl +
+ ": " +
+ waitForUpdateSec +
+ "\n"
+ );
+
+ // The time unit below are all milliseconds if not specified.
+
+ var delay = 0;
+ if (waitForUpdateSec) {
+ delay = parseInt(waitForUpdateSec, 10) * 1000;
+ }
+ // As long as the delay is something sane (5 min to 1 day), update
+ // our delay time for requesting updates. We always use a non-repeating
+ // timer since the delay is set differently at every callback.
+ if (delay > maxDelayMs) {
+ log(
+ "Ignoring delay from server (too long), waiting " +
+ Math.round(maxDelayMs / 60000) +
+ "min"
+ );
+ delay = maxDelayMs;
+ } else if (delay < minDelayMs) {
+ log(
+ "Ignoring delay from server (too short), waiting " +
+ Math.round(this.updateInterval / 60000) +
+ "min"
+ );
+ delay = this.updateInterval;
+ } else {
+ log("Waiting " + Math.round(delay / 60000) + "min");
+ }
+
+ this.setUpdateCheckTimer(updateUrl, delay);
+
+ // Let the backoff object know that we completed successfully.
+ this.requestBackoffs_[updateUrl].noteServerResponse(200);
+
+ // Set last update time for provider
+ // Get the provider for these tables, check for consistency
+ let tables = tableList.split(",");
+ let provider = null;
+ for (let table of tables) {
+ let newProvider = this.tablesData[table].provider;
+ if (provider) {
+ if (newProvider !== provider) {
+ log(
+ "Multiple tables for the same updateURL have a different provider?!"
+ );
+ }
+ } else {
+ provider = newProvider;
+ }
+ }
+
+ // Store the last update time (needed to know if the table is "fresh")
+ // and the next update time (to know when to update next).
+ let lastUpdatePref =
+ "browser.safebrowsing.provider." + provider + ".lastupdatetime";
+ let now = Date.now();
+ log("Setting last update of " + provider + " to " + now);
+ Services.prefs.setCharPref(lastUpdatePref, now.toString());
+
+ let nextUpdatePref =
+ "browser.safebrowsing.provider." + provider + ".nextupdatetime";
+ let targetTime = now + delay;
+ log(
+ "Setting next update of " +
+ provider +
+ " to " +
+ targetTime +
+ " (" +
+ Math.round(delay / 60000) +
+ "min from now)"
+ );
+ Services.prefs.setCharPref(nextUpdatePref, targetTime.toString());
+
+ Services.obs.notifyObservers(null, "safebrowsing-update-finished", "success");
+};
+
+/**
+ * Callback function if the update request succeeded.
+ * @param result String The error code of the failure
+ */
+PROT_ListManager.prototype.updateError_ = function(table, updateUrl, result) {
+ log(
+ "update error for " + table + " from " + updateUrl + ": " + result + "\n"
+ );
+ // There was some trouble applying the updates. Don't try again for at least
+ // updateInterval milliseconds.
+ this.setUpdateCheckTimer(updateUrl, this.updateInterval);
+
+ Services.obs.notifyObservers(
+ null,
+ "safebrowsing-update-finished",
+ "update error: " + result
+ );
+};
+
+/**
+ * Callback function when the download failed
+ * @param status String http status or an empty string if connection refused.
+ */
+PROT_ListManager.prototype.downloadError_ = function(table, updateUrl, status) {
+ log("download error for " + table + ": " + status + "\n");
+ // If status is empty, then we assume that we got an NS_CONNECTION_REFUSED
+ // error. In this case, we treat this is a http 500 error.
+ if (!status) {
+ status = 500;
+ }
+ status = parseInt(status, 10);
+ this.requestBackoffs_[updateUrl].noteServerResponse(status);
+ var delay = this.updateInterval;
+ if (this.requestBackoffs_[updateUrl].isErrorStatus(status)) {
+ // Schedule an update for when our backoff is complete
+ delay = this.requestBackoffs_[updateUrl].nextRequestDelay();
+ } else {
+ log("Got non error status for error callback?!");
+ }
+
+ this.setUpdateCheckTimer(updateUrl, delay);
+
+ Services.obs.notifyObservers(
+ null,
+ "safebrowsing-update-finished",
+ "download error: " + status
+ );
+};
+
+/**
+ * Get back-off time for the given provider.
+ * Return 0 if we are not in back-off mode.
+ */
+PROT_ListManager.prototype.getBackOffTime = function(provider) {
+ let updateUrl = "";
+ for (var table in this.tablesData) {
+ if (this.tablesData[table].provider == provider) {
+ updateUrl = this.tablesData[table].updateUrl;
+ break;
+ }
+ }
+
+ if (!updateUrl || !this.requestBackoffs_[updateUrl]) {
+ return 0;
+ }
+
+ let delay = this.requestBackoffs_[updateUrl].nextRequestDelay();
+ return delay == 0 ? 0 : Date.now() + delay;
+};
+
+PROT_ListManager.prototype.QueryInterface = ChromeUtils.generateQI([
+ "nsIUrlListManager",
+ "nsIObserver",
+ "nsITimerCallback",
+]);
+
+let initialized = false;
+function Init() {
+ if (initialized) {
+ return;
+ }
+
+ // Pull the library in.
+ var jslib = Cc["@mozilla.org/url-classifier/jslib;1"].getService()
+ .wrappedJSObject;
+ BindToObject = jslib.BindToObject;
+ RequestBackoffV4 = jslib.RequestBackoffV4;
+
+ initialized = true;
+}
+
+function RegistrationData() {
+ Init();
+ return new PROT_ListManager();
+}
+
+const lazy = {};
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "enableTestNotifications",
+ PREF_TEST_NOTIFICATIONS,
+ false
+);
+
+var EXPORTED_SYMBOLS = ["RegistrationData"];