summaryrefslogtreecommitdiffstats
path: root/toolkit/components/url-classifier/content
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/url-classifier/content')
-rw-r--r--toolkit/components/url-classifier/content/listmanager.js601
-rw-r--r--toolkit/components/url-classifier/content/moz/alarm.js157
-rw-r--r--toolkit/components/url-classifier/content/moz/cryptohasher.js176
-rw-r--r--toolkit/components/url-classifier/content/moz/debug.js867
-rw-r--r--toolkit/components/url-classifier/content/moz/lang.js82
-rw-r--r--toolkit/components/url-classifier/content/moz/observer.js145
-rw-r--r--toolkit/components/url-classifier/content/moz/preferences.js276
-rw-r--r--toolkit/components/url-classifier/content/moz/protocol4.js133
-rw-r--r--toolkit/components/url-classifier/content/multi-querier.js137
-rw-r--r--toolkit/components/url-classifier/content/request-backoff.js116
-rw-r--r--toolkit/components/url-classifier/content/trtable.js169
-rw-r--r--toolkit/components/url-classifier/content/wireformat.js230
-rw-r--r--toolkit/components/url-classifier/content/xml-fetcher.js126
13 files changed, 3215 insertions, 0 deletions
diff --git a/toolkit/components/url-classifier/content/listmanager.js b/toolkit/components/url-classifier/content/listmanager.js
new file mode 100644
index 000000000..68325bec8
--- /dev/null
+++ b/toolkit/components/url-classifier/content/listmanager.js
@@ -0,0 +1,601 @@
+# 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/.
+
+var Cu = Components.utils;
+Cu.import("resource://gre/modules/Services.jsm");
+
+// This is the only implementation of nsIUrlListManager.
+// A class that manages lists, namely white and black 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;
+
+// Log only if browser.safebrowsing.debug is true
+this.log = function log(...stuff) {
+ var prefs_ = new G_Preferences();
+ var debug = prefs_.getPref("browser.safebrowsing.debug");
+ if (!debug) {
+ return;
+ }
+
+ var d = new Date();
+ let msg = "listmanager: " + d.toTimeString() + ": " + stuff.join(" ");
+ msg = Services.urlFormatter.trimSensitiveURLs(msg);
+ Services.console.logStringMessage(msg);
+ dump(msg + "\n");
+}
+
+this.QueryAdapter = function QueryAdapter(callback) {
+ this.callback_ = callback;
+};
+
+QueryAdapter.prototype.handleResponse = function(value) {
+ this.callback_.handleEvent(value);
+}
+
+/**
+ * A ListManager keeps track of black and white lists and knows
+ * how to update them.
+ *
+ * @constructor
+ */
+this.PROT_ListManager = function PROT_ListManager() {
+ log("Initializing list manager");
+ this.prefs_ = new G_Preferences();
+ this.updateInterval = this.prefs_.getPref("urlclassifier.updateinterval", 30 * 60) * 1000;
+
+ // 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_ = {};
+
+ this.observerServiceObserver_ = new G_ObserverServiceObserver(
+ 'quit-application',
+ BindToObject(this.shutdown_, this),
+ true /*only once*/);
+
+ // A map of updateUrls to single-use G_Alarms. An entry exists if and only if
+ // there is at least one table with updates enabled for that url. G_Alarms
+ // are reset when enabling/disabling updates or on update callbacks (update
+ // success, update failure, download error).
+ this.updateCheckers_ = {};
+ this.requestBackoffs_ = {};
+ this.dbService_ = Cc["@mozilla.org/url-classifier/dbservice;1"]
+ .getService(Ci.nsIUrlClassifierDBService);
+
+
+ this.hashCompleter_ = Cc["@mozilla.org/url-classifier/hashcompleter;1"]
+ .getService(Ci.nsIUrlClassifierHashCompleter);
+}
+
+/**
+ * xpcom-shutdown callback
+ * Delete all of our data tables which seem to leak otherwise.
+ */
+PROT_ListManager.prototype.shutdown_ = function() {
+ for (var name in this.tablesData) {
+ delete this.tablesData[name];
+ }
+}
+
+/**
+ * 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) {
+ log("registering " + tableName + " with " + updateUrl);
+ if (!updateUrl) {
+ log("Can't register table " + tableName + " without updateUrl");
+ return false;
+ }
+ this.tablesData[tableName] = {};
+ 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 */);
+ }
+ this.needsUpdate_[updateUrl][tableName] = false;
+
+ return true;
+}
+
+PROT_ListManager.prototype.getGethashUrl = function(tableName) {
+ if (this.tablesData[tableName] && this.tablesData[tableName].gethashUrl) {
+ return this.tablesData[tableName].gethashUrl;
+ }
+ return "";
+}
+
+/**
+ * Enable updates for some tables
+ * @param tables - an array of table names that need updating
+ */
+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;
+ }
+}
+
+/**
+ * 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;
+}
+
+/**
+ * Disables updates for some tables
+ * @param tables - an array of table names that no longer need updating
+ */
+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;
+}
+
+/**
+ * Acts as a nsIUrlClassifierCallback for getTables.
+ */
+PROT_ListManager.prototype.kickoffUpdate_ = function (onDiskTableData)
+{
+ 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 targetPref = "browser.safebrowsing.provider." + provider + ".nextupdatetime";
+ let nextUpdate = this.prefs_.getPref(targetPref);
+ if (nextUpdate) {
+ updateDelay = Math.min(maxDelayMs, Math.max(0, nextUpdate - Date.now()));
+ log("Next update at " + nextUpdate);
+ }
+ log("Next update " + updateDelay + "ms from now");
+
+ // Set the last update time to verify if data is still valid.
+ let freshnessPref = "browser.safebrowsing.provider." + provider + ".lastupdatetime";
+ let freshness = this.prefs_.getPref(freshnessPref);
+ if (freshness) {
+ Object.keys(this.tablesData).forEach(function(table) {
+ if (this.tablesData[table].provider === provider) {
+ this.dbService_.setLastUpdateTime(table, freshness);
+ }}, this);
+ }
+
+ this.updateCheckers_[updateUrl] =
+ new G_Alarm(BindToObject(this.checkForUpdates, this, updateUrl),
+ updateDelay, false /* repeating */);
+ } 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.dbService_.getTables(BindToObject(this.kickoffUpdate_, this));
+ }
+ } else {
+ log("Stopping managing lists (if currently active)");
+ this.stopUpdateCheckers(); // Cancel pending updates
+ }
+}
+
+/**
+ * Provides an exception free way to look up the data in a table. We
+ * use this because at certain points our tables might not be loaded,
+ * and querying them could throw.
+ *
+ * @param table String Name of the table that we want to consult
+ * @param key Principal being used to lookup the database
+ * @param callback nsIUrlListManagerCallback (ie., Function) given false or the
+ * value in the table corresponding to key. If the table name does not
+ * exist, we return false, too.
+ */
+PROT_ListManager.prototype.safeLookup = function(key, callback) {
+ try {
+ log("safeLookup: " + key);
+ var cb = new QueryAdapter(callback);
+ this.dbService_.lookup(key,
+ BindToObject(cb.handleResponse, cb),
+ true);
+ } catch(e) {
+ log("safeLookup masked failure for key " + key + ": " + e);
+ callback.handleEvent("");
+ }
+}
+
+/**
+ * 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.
+ */
+PROT_ListManager.prototype.checkForUpdates = function(updateUrl) {
+ log("checkForUpdates with " + updateUrl);
+ // See if we've triggered the request backoff logic.
+ if (!updateUrl) {
+ return false;
+ }
+ 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);
+ 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,
+ tableArray.length);
+ 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 > 0) {
+ 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");
+ }
+}
+
+/**
+ * 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 " +
+ maxDelayMs + "ms");
+ delay = maxDelayMs;
+ } else if (delay < minDelayMs) {
+ log("Ignoring delay from server (too short), waiting " +
+ this.updateInterval + "ms");
+ delay = this.updateInterval;
+ } else {
+ log("Waiting " + delay + "ms");
+ }
+ this.updateCheckers_[updateUrl] =
+ new G_Alarm(BindToObject(this.checkForUpdates, this, updateUrl),
+ delay, false);
+
+ // 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);
+ this.prefs_.setPref(lastUpdatePref, now.toString());
+
+ let nextUpdatePref = "browser.safebrowsing.provider." + provider + ".nextupdatetime";
+ let targetTime = now + delay;
+ log("Setting next update of " + provider + " to " + targetTime
+ + " (" + delay + "ms from now)");
+ this.prefs_.setPref(nextUpdatePref, targetTime.toString());
+}
+
+/**
+ * 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.updateCheckers_[updateUrl] =
+ new G_Alarm(BindToObject(this.checkForUpdates, this, updateUrl),
+ this.updateInterval, false);
+}
+
+/**
+ * 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.updateCheckers_[updateUrl] =
+ new G_Alarm(BindToObject(this.checkForUpdates, this, updateUrl),
+ delay, false);
+
+}
+
+PROT_ListManager.prototype.QueryInterface = function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIUrlListManager) ||
+ iid.equals(Ci.nsITimerCallback))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+}
diff --git a/toolkit/components/url-classifier/content/moz/alarm.js b/toolkit/components/url-classifier/content/moz/alarm.js
new file mode 100644
index 000000000..7de067546
--- /dev/null
+++ b/toolkit/components/url-classifier/content/moz/alarm.js
@@ -0,0 +1,157 @@
+# 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/.
+
+
+// An Alarm fires a callback after a certain amount of time, or at
+// regular intervals. It's a convenient replacement for
+// setTimeout/Interval when you don't want to bind to a specific
+// window.
+//
+// The ConditionalAlarm is an Alarm that cancels itself if its callback
+// returns a value that type-converts to true.
+//
+// Example:
+//
+// function foo() { dump('hi'); };
+// new G_Alarm(foo, 10*1000); // Fire foo in 10 seconds
+// new G_Alarm(foo, 10*1000, true /*repeat*/); // Fire foo every 10 seconds
+// new G_Alarm(foo, 10*1000, true, 7); // Fire foo every 10 seconds
+// // seven times
+// new G_ConditionalAlarm(foo, 1000, true); // Fire every sec until foo()==true
+//
+// // Fire foo every 10 seconds until foo returns true or until it fires seven
+// // times, whichever happens first.
+// new G_ConditionalAlarm(foo, 10*1000, true /*repeating*/, 7);
+//
+// TODO: maybe pass an isFinal flag to the callback if they opted to
+// set maxTimes and this is the last iteration?
+
+
+/**
+ * Set an alarm to fire after a given amount of time, or at specific
+ * intervals.
+ *
+ * @param callback Function to call when the alarm fires
+ * @param delayMS Number indicating the length of the alarm period in ms
+ * @param opt_repeating Boolean indicating whether this should fire
+ * periodically
+ * @param opt_maxTimes Number indicating a maximum number of times to
+ * repeat (obviously only useful when opt_repeating==true)
+ */
+this.G_Alarm =
+function G_Alarm(callback, delayMS, opt_repeating, opt_maxTimes) {
+ this.debugZone = "alarm";
+ this.callback_ = callback;
+ this.repeating_ = !!opt_repeating;
+ this.timer_ = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ var type = opt_repeating ?
+ this.timer_.TYPE_REPEATING_SLACK :
+ this.timer_.TYPE_ONE_SHOT;
+ this.maxTimes_ = opt_maxTimes ? opt_maxTimes : null;
+ this.nTimes_ = 0;
+
+ this.observerServiceObserver_ = new G_ObserverServiceObserver(
+ 'xpcom-shutdown',
+ BindToObject(this.cancel, this));
+
+ // Ask the timer to use nsITimerCallback (.notify()) when ready
+ this.timer_.initWithCallback(this, delayMS, type);
+}
+
+/**
+ * Cancel this timer
+ */
+G_Alarm.prototype.cancel = function() {
+ if (!this.timer_) {
+ return;
+ }
+
+ this.timer_.cancel();
+ // Break circular reference created between this.timer_ and the G_Alarm
+ // instance (this)
+ this.timer_ = null;
+ this.callback_ = null;
+
+ // We don't need the shutdown observer anymore
+ this.observerServiceObserver_.unregister();
+}
+
+/**
+ * Invoked by the timer when it fires
+ *
+ * @param timer Reference to the nsITimer which fired (not currently
+ * passed along)
+ */
+G_Alarm.prototype.notify = function(timer) {
+ // fire callback and save results
+ var ret = this.callback_();
+
+ // If they've given us a max number of times to fire, enforce it
+ this.nTimes_++;
+ if (this.repeating_ &&
+ typeof this.maxTimes_ == "number"
+ && this.nTimes_ >= this.maxTimes_) {
+ this.cancel();
+ } else if (!this.repeating_) {
+ // Clear out the callback closure for TYPE_ONE_SHOT timers
+ this.cancel();
+ }
+ // We don't cancel/cleanup timers that repeat forever until either
+ // xpcom-shutdown occurs or cancel() is called explicitly.
+
+ return ret;
+}
+
+G_Alarm.prototype.setDelay = function(delay) {
+ this.timer_.delay = delay;
+}
+
+/**
+ * XPCOM cruft
+ */
+G_Alarm.prototype.QueryInterface = function(iid) {
+ if (iid.equals(Components.interfaces.nsISupports) ||
+ iid.equals(Components.interfaces.nsITimerCallback))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+}
+
+
+/**
+ * An alarm with the additional property that it cancels itself if its
+ * callback returns true.
+ *
+ * For parameter documentation, see G_Alarm
+ */
+this.G_ConditionalAlarm =
+function G_ConditionalAlarm(callback, delayMS, opt_repeating, opt_maxTimes) {
+ G_Alarm.call(this, callback, delayMS, opt_repeating, opt_maxTimes);
+ this.debugZone = "conditionalalarm";
+}
+
+G_ConditionalAlarm.inherits = function(parentCtor) {
+ var tempCtor = function(){};
+ tempCtor.prototype = parentCtor.prototype;
+ this.superClass_ = parentCtor.prototype;
+ this.prototype = new tempCtor();
+}
+
+G_ConditionalAlarm.inherits(G_Alarm);
+
+/**
+ * Invoked by the timer when it fires
+ *
+ * @param timer Reference to the nsITimer which fired (not currently
+ * passed along)
+ */
+G_ConditionalAlarm.prototype.notify = function(timer) {
+ // Call G_Alarm::notify
+ var rv = G_Alarm.prototype.notify.call(this, timer);
+
+ if (this.repeating_ && rv) {
+ G_Debug(this, "Callback of a repeating alarm returned true; cancelling.");
+ this.cancel();
+ }
+}
diff --git a/toolkit/components/url-classifier/content/moz/cryptohasher.js b/toolkit/components/url-classifier/content/moz/cryptohasher.js
new file mode 100644
index 000000000..a1294aa93
--- /dev/null
+++ b/toolkit/components/url-classifier/content/moz/cryptohasher.js
@@ -0,0 +1,176 @@
+# 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/.
+
+
+// A very thin wrapper around nsICryptoHash. It's not strictly
+// necessary, but makes the code a bit cleaner and gives us the
+// opportunity to verify that our implementations give the results that
+// we expect, for example if we have to interoperate with a server.
+//
+// The digest* methods reset the state of the hasher, so it's
+// necessary to call init() explicitly after them.
+//
+// Works only in Firefox 1.5+.
+//
+// IMPORTANT NOTE: Due to https://bugzilla.mozilla.org/show_bug.cgi?id=321024
+// you cannot use the cryptohasher before app-startup. The symptom of doing
+// so is a segfault in NSS.
+
+/**
+ * Instantiate a new hasher. You must explicitly call init() before use!
+ */
+this.G_CryptoHasher =
+function G_CryptoHasher() {
+ this.debugZone = "cryptohasher";
+ this.hasher_ = null;
+}
+
+G_CryptoHasher.algorithms = {
+ MD2: Ci.nsICryptoHash.MD2,
+ MD5: Ci.nsICryptoHash.MD5,
+ SHA1: Ci.nsICryptoHash.SHA1,
+ SHA256: Ci.nsICryptoHash.SHA256,
+ SHA384: Ci.nsICryptoHash.SHA384,
+ SHA512: Ci.nsICryptoHash.SHA512,
+};
+
+/**
+ * Initialize the hasher. This function must be called after every call
+ * to one of the digest* methods.
+ *
+ * @param algorithm Constant from G_CryptoHasher.algorithms specifying the
+ * algorithm this hasher will use
+ */
+G_CryptoHasher.prototype.init = function(algorithm) {
+ var validAlgorithm = false;
+ for (var alg in G_CryptoHasher.algorithms)
+ if (algorithm == G_CryptoHasher.algorithms[alg])
+ validAlgorithm = true;
+
+ if (!validAlgorithm)
+ throw new Error("Invalid algorithm: " + algorithm);
+
+ this.hasher_ = Cc["@mozilla.org/security/hash;1"]
+ .createInstance(Ci.nsICryptoHash);
+ this.hasher_.init(algorithm);
+}
+
+/**
+ * Update the hash's internal state with input given in a string. Can be
+ * called multiple times for incrementeal hash updates.
+ *
+ * @param input String containing data to hash.
+ */
+G_CryptoHasher.prototype.updateFromString = function(input) {
+ if (!this.hasher_)
+ throw new Error("You must initialize the hasher first!");
+
+ var stream = Cc['@mozilla.org/io/string-input-stream;1']
+ .createInstance(Ci.nsIStringInputStream);
+ stream.setData(input, input.length);
+ this.updateFromStream(stream);
+}
+
+/**
+ * Update the hash's internal state with input given in an array. Can be
+ * called multiple times for incremental hash updates.
+ *
+ * @param input Array containing data to hash.
+ */
+G_CryptoHasher.prototype.updateFromArray = function(input) {
+ if (!this.hasher_)
+ throw new Error("You must initialize the hasher first!");
+
+ this.hasher_.update(input, input.length);
+}
+
+/**
+ * Update the hash's internal state with input given in a stream. Can be
+ * called multiple times from incremental hash updates.
+ */
+G_CryptoHasher.prototype.updateFromStream = function(stream) {
+ if (!this.hasher_)
+ throw new Error("You must initialize the hasher first!");
+
+ if (stream.available())
+ this.hasher_.updateFromStream(stream, stream.available());
+}
+
+/**
+ * @returns The hash value as a string (sequence of 8-bit values)
+ */
+G_CryptoHasher.prototype.digestRaw = function() {
+ var digest = this.hasher_.finish(false /* not b64 encoded */);
+ this.hasher_ = null;
+ return digest;
+}
+
+/**
+ * @returns The hash value as a base64-encoded string
+ */
+G_CryptoHasher.prototype.digestBase64 = function() {
+ var digest = this.hasher_.finish(true /* b64 encoded */);
+ this.hasher_ = null;
+ return digest;
+}
+
+/**
+ * @returns The hash value as a hex-encoded string
+ */
+G_CryptoHasher.prototype.digestHex = function() {
+ var raw = this.digestRaw();
+ return this.toHex_(raw);
+}
+
+/**
+ * Converts a sequence of values to a hex-encoded string. The input is a
+ * a string, so you can stick 16-bit values in each character.
+ *
+ * @param str String to conver to hex. (Often this is just a sequence of
+ * 16-bit values)
+ *
+ * @returns String containing the hex representation of the input
+ */
+G_CryptoHasher.prototype.toHex_ = function(str) {
+ var hexchars = '0123456789ABCDEF';
+ var hexrep = new Array(str.length * 2);
+
+ for (var i = 0; i < str.length; ++i) {
+ hexrep[i * 2] = hexchars.charAt((str.charCodeAt(i) >> 4) & 15);
+ hexrep[i * 2 + 1] = hexchars.charAt(str.charCodeAt(i) & 15);
+ }
+ return hexrep.join('');
+}
+
+#ifdef DEBUG
+/**
+ * Lame unittest function
+ */
+this.TEST_G_CryptoHasher = function TEST_G_CryptoHasher() {
+ if (G_GDEBUG) {
+ var z = "cryptohasher UNITTEST";
+ G_debugService.enableZone(z);
+
+ G_Debug(z, "Starting");
+
+ var md5 = function(str) {
+ var hasher = new G_CryptoHasher();
+ hasher.init(G_CryptoHasher.algorithms.MD5);
+ hasher.updateFromString(str);
+ return hasher.digestHex().toLowerCase();
+ };
+
+ // test vectors from: http://www.faqs.org/rfcs/rfc1321.html
+ var vectors = {"": "d41d8cd98f00b204e9800998ecf8427e",
+ "a": "0cc175b9c0f1b6a831c399e269772661",
+ "abc": "900150983cd24fb0d6963f7d28e17f72",
+ "message digest": "f96b697d7cb7938d525a2f31aaf161d0",
+ "abcdefghijklmnopqrstuvwxyz": "c3fcd3d76192e4007dfb496cca67e13b",
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789": "d174ab98d277d9f5a5611c2c9f419d9f",
+ "12345678901234567890123456789012345678901234567890123456789012345678901234567890": "57edf4a22be3c955ac49da2e2107b67a"};
+
+ G_Debug(z, "PASSED");
+ }
+}
+#endif
diff --git a/toolkit/components/url-classifier/content/moz/debug.js b/toolkit/components/url-classifier/content/moz/debug.js
new file mode 100644
index 000000000..ed4c11793
--- /dev/null
+++ b/toolkit/components/url-classifier/content/moz/debug.js
@@ -0,0 +1,867 @@
+# 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/.
+
+#ifdef DEBUG
+
+// Generic logging/debugging functionality that:
+//
+// (*) when disabled compiles to no-ops at worst (for calls to the service)
+// and to nothing at best (calls to G_Debug() and similar are compiled
+// away when you use a jscompiler that strips dead code)
+//
+// (*) has dynamically configurable/creatable debugging "zones" enabling
+// selective logging
+//
+// (*) hides its plumbing so that all calls in different zones are uniform,
+// so you can drop files using this library into other apps that use it
+// without any configuration
+//
+// (*) can be controlled programmatically or via preferences. The
+// preferences that control the service and its zones are under
+// the preference branch "safebrowsing-debug-service."
+//
+// (*) outputs function call traces when the "loggifier" zone is enabled
+//
+// (*) can write output to logfiles so that you can get a call trace
+// from someone who is having a problem
+//
+// Example:
+//
+// var G_GDEBUG = true // Enable this module
+// var G_debugService = new G_DebugService(); // in global context
+//
+// // You can use it with arbitrary primitive first arguement
+// G_Debug("myzone", "Yo yo yo"); // outputs: [myzone] Yo yo yo\n
+//
+// // But it's nice to use it with an object; it will probe for the zone name
+// function Obj() {
+// this.debugZone = "someobj";
+// }
+// Obj.prototype.foo = function() {
+// G_Debug(this, "foo called");
+// }
+// (new Obj).foo(); // outputs: [someobj] foo called\n
+//
+// G_debugService.loggifier.loggify(Obj.prototype); // enable call tracing
+//
+// // En/disable specific zones programmatically (you can also use preferences)
+// G_debugService.enableZone("somezone");
+// G_debugService.disableZone("someotherzone");
+// G_debugService.enableAllZones();
+//
+// // We also have asserts and errors:
+// G_Error(this, "Some error occurred"); // will throw
+// G_Assert(this, (x > 3), "x not greater than three!"); // will throw
+//
+// See classes below for more methods.
+//
+// TODO add code to set prefs when not found to the default value of a tristate
+// TODO add error level support
+// TODO add ability to turn off console output
+//
+// -------> TO START DEBUGGING: set G_GDEBUG to true
+
+// These are the functions code will typically call. Everything is
+// wrapped in if's so we can compile it away when G_GDEBUG is false.
+
+
+if (typeof G_GDEBUG == "undefined") {
+ throw new Error("G_GDEBUG constant must be set before loading debug.js");
+}
+
+
+/**
+ * Write out a debugging message.
+ *
+ * @param who The thingy to convert into a zone name corresponding to the
+ * zone to which this message belongs
+ * @param msg Message to output
+ */
+this.G_Debug = function G_Debug(who, msg) {
+ if (G_GDEBUG) {
+ G_GetDebugZone(who).debug(msg);
+ }
+}
+
+/**
+ * Debugs loudly
+ */
+this.G_DebugL = function G_DebugL(who, msg) {
+ if (G_GDEBUG) {
+ var zone = G_GetDebugZone(who);
+
+ if (zone.zoneIsEnabled()) {
+ G_debugService.dump(
+ "\n************************************************************\n");
+
+ G_Debug(who, msg);
+
+ G_debugService.dump(
+ "************************************************************\n\n");
+ }
+ }
+}
+
+/**
+ * Write out a call tracing message
+ *
+ * @param who The thingy to convert into a zone name corresponding to the
+ * zone to which this message belongs
+ * @param msg Message to output
+ */
+this.G_TraceCall = function G_TraceCall(who, msg) {
+ if (G_GDEBUG) {
+ if (G_debugService.callTracingEnabled()) {
+ G_debugService.dump(msg + "\n");
+ }
+ }
+}
+
+/**
+ * Write out an error (and throw)
+ *
+ * @param who The thingy to convert into a zone name corresponding to the
+ * zone to which this message belongs
+ * @param msg Message to output
+ */
+this.G_Error = function G_Error(who, msg) {
+ if (G_GDEBUG) {
+ G_GetDebugZone(who).error(msg);
+ }
+}
+
+/**
+ * Assert something as true and signal an error if it's not
+ *
+ * @param who The thingy to convert into a zone name corresponding to the
+ * zone to which this message belongs
+ * @param condition Boolean condition to test
+ * @param msg Message to output
+ */
+this.G_Assert = function G_Assert(who, condition, msg) {
+ if (G_GDEBUG) {
+ G_GetDebugZone(who).assert(condition, msg);
+ }
+}
+
+/**
+ * Helper function that takes input and returns the DebugZone
+ * corresponding to it.
+ *
+ * @param who Arbitrary input that will be converted into a zone name. Most
+ * likely an object that has .debugZone property, or a string.
+ * @returns The DebugZone object corresponding to the input
+ */
+this.G_GetDebugZone = function G_GetDebugZone(who) {
+ if (G_GDEBUG) {
+ var zone = "?";
+
+ if (who && who.debugZone) {
+ zone = who.debugZone;
+ } else if (typeof who == "string") {
+ zone = who;
+ }
+
+ return G_debugService.getZone(zone);
+ }
+}
+
+// Classes that implement the functionality.
+
+/**
+ * A debug "zone" is a string derived from arbitrary types (but
+ * typically derived from another string or an object). All debugging
+ * messages using a particular zone can be enabled or disabled
+ * independent of other zones. This enables you to turn on/off logging
+ * of particular objects or modules. This object implements a single
+ * zone and the methods required to use it.
+ *
+ * @constructor
+ * @param service Reference to the DebugService object we use for
+ * registration
+ * @param prefix String indicating the unique prefix we should use
+ * when creating preferences to control this zone
+ * @param zone String indicating the name of the zone
+ */
+this.G_DebugZone = function G_DebugZone(service, prefix, zone) {
+ if (G_GDEBUG) {
+ this.debugService_ = service;
+ this.prefix_ = prefix;
+ this.zone_ = zone;
+ this.zoneEnabledPrefName_ = prefix + ".zone." + this.zone_;
+ this.settings_ = new G_DebugSettings();
+ }
+}
+
+/**
+ * @returns Boolean indicating if this zone is enabled
+ */
+G_DebugZone.prototype.zoneIsEnabled = function() {
+ if (G_GDEBUG) {
+ var explicit = this.settings_.getSetting(this.zoneEnabledPrefName_, null);
+
+ if (explicit !== null) {
+ return explicit;
+ } else {
+ return this.debugService_.allZonesEnabled();
+ }
+ }
+}
+
+/**
+ * Enable this logging zone
+ */
+G_DebugZone.prototype.enableZone = function() {
+ if (G_GDEBUG) {
+ this.settings_.setDefault(this.zoneEnabledPrefName_, true);
+ }
+}
+
+/**
+ * Disable this logging zone
+ */
+G_DebugZone.prototype.disableZone = function() {
+ if (G_GDEBUG) {
+ this.settings_.setDefault(this.zoneEnabledPrefName_, false);
+ }
+}
+
+/**
+ * Write a debugging message to this zone
+ *
+ * @param msg String of message to write
+ */
+G_DebugZone.prototype.debug = function(msg) {
+ if (G_GDEBUG) {
+ if (this.zoneIsEnabled()) {
+ this.debugService_.dump("[" + this.zone_ + "] " + msg + "\n");
+ }
+ }
+}
+
+/**
+ * Write an error to this zone and throw
+ *
+ * @param msg String of error to write
+ */
+G_DebugZone.prototype.error = function(msg) {
+ if (G_GDEBUG) {
+ this.debugService_.dump("[" + this.zone_ + "] " + msg + "\n");
+ throw new Error(msg);
+ debugger;
+ }
+}
+
+/**
+ * Assert something as true and error if it is not
+ *
+ * @param condition Boolean condition to test
+ * @param msg String of message to write if is false
+ */
+G_DebugZone.prototype.assert = function(condition, msg) {
+ if (G_GDEBUG) {
+ if (condition !== true) {
+ G_Error(this.zone_, "ASSERT FAILED: " + msg);
+ }
+ }
+}
+
+
+/**
+ * The debug service handles auto-registration of zones, namespacing
+ * the zones preferences, and various global settings such as whether
+ * all zones are enabled.
+ *
+ * @constructor
+ * @param opt_prefix Optional string indicating the unique prefix we should
+ * use when creating preferences
+ */
+this.G_DebugService = function G_DebugService(opt_prefix) {
+ if (G_GDEBUG) {
+ this.prefix_ = opt_prefix ? opt_prefix : "safebrowsing-debug-service";
+ this.consoleEnabledPrefName_ = this.prefix_ + ".alsologtoconsole";
+ this.allZonesEnabledPrefName_ = this.prefix_ + ".enableallzones";
+ this.callTracingEnabledPrefName_ = this.prefix_ + ".trace-function-calls";
+ this.logFileEnabledPrefName_ = this.prefix_ + ".logfileenabled";
+ this.logFileErrorLevelPrefName_ = this.prefix_ + ".logfile-errorlevel";
+ this.zones_ = {};
+
+ this.loggifier = new G_Loggifier();
+ this.settings_ = new G_DebugSettings();
+ }
+}
+
+// Error levels for reporting console messages to the log.
+G_DebugService.ERROR_LEVEL_INFO = "INFO";
+G_DebugService.ERROR_LEVEL_WARNING = "WARNING";
+G_DebugService.ERROR_LEVEL_EXCEPTION = "EXCEPTION";
+
+
+/**
+ * @returns Boolean indicating if we should send messages to the jsconsole
+ */
+G_DebugService.prototype.alsoDumpToConsole = function() {
+ if (G_GDEBUG) {
+ return this.settings_.getSetting(this.consoleEnabledPrefName_, false);
+ }
+}
+
+/**
+ * @returns whether to log output to a file as well as the console.
+ */
+G_DebugService.prototype.logFileIsEnabled = function() {
+ if (G_GDEBUG) {
+ return this.settings_.getSetting(this.logFileEnabledPrefName_, false);
+ }
+}
+
+/**
+ * Turns on file logging. dump() output will also go to the file specified by
+ * setLogFile()
+ */
+G_DebugService.prototype.enableLogFile = function() {
+ if (G_GDEBUG) {
+ this.settings_.setDefault(this.logFileEnabledPrefName_, true);
+ }
+}
+
+/**
+ * Turns off file logging
+ */
+G_DebugService.prototype.disableLogFile = function() {
+ if (G_GDEBUG) {
+ this.settings_.setDefault(this.logFileEnabledPrefName_, false);
+ }
+}
+
+/**
+ * @returns an nsIFile instance pointing to the current log file location
+ */
+G_DebugService.prototype.getLogFile = function() {
+ if (G_GDEBUG) {
+ return this.logFile_;
+ }
+}
+
+/**
+ * Sets a new log file location
+ */
+G_DebugService.prototype.setLogFile = function(file) {
+ if (G_GDEBUG) {
+ this.logFile_ = file;
+ }
+}
+
+/**
+ * Enables sending messages to the jsconsole
+ */
+G_DebugService.prototype.enableDumpToConsole = function() {
+ if (G_GDEBUG) {
+ this.settings_.setDefault(this.consoleEnabledPrefName_, true);
+ }
+}
+
+/**
+ * Disables sending messages to the jsconsole
+ */
+G_DebugService.prototype.disableDumpToConsole = function() {
+ if (G_GDEBUG) {
+ this.settings_.setDefault(this.consoleEnabledPrefName_, false);
+ }
+}
+
+/**
+ * @param zone Name of the zone to get
+ * @returns The DebugZone object corresopnding to input. If not such
+ * zone exists, a new one is created and returned
+ */
+G_DebugService.prototype.getZone = function(zone) {
+ if (G_GDEBUG) {
+ if (!this.zones_[zone])
+ this.zones_[zone] = new G_DebugZone(this, this.prefix_, zone);
+
+ return this.zones_[zone];
+ }
+}
+
+/**
+ * @param zone Zone to enable debugging for
+ */
+G_DebugService.prototype.enableZone = function(zone) {
+ if (G_GDEBUG) {
+ var toEnable = this.getZone(zone);
+ toEnable.enableZone();
+ }
+}
+
+/**
+ * @param zone Zone to disable debugging for
+ */
+G_DebugService.prototype.disableZone = function(zone) {
+ if (G_GDEBUG) {
+ var toDisable = this.getZone(zone);
+ toDisable.disableZone();
+ }
+}
+
+/**
+ * @returns Boolean indicating whether debugging is enabled for all zones
+ */
+G_DebugService.prototype.allZonesEnabled = function() {
+ if (G_GDEBUG) {
+ return this.settings_.getSetting(this.allZonesEnabledPrefName_, false);
+ }
+}
+
+/**
+ * Enables all debugging zones
+ */
+G_DebugService.prototype.enableAllZones = function() {
+ if (G_GDEBUG) {
+ this.settings_.setDefault(this.allZonesEnabledPrefName_, true);
+ }
+}
+
+/**
+ * Disables all debugging zones
+ */
+G_DebugService.prototype.disableAllZones = function() {
+ if (G_GDEBUG) {
+ this.settings_.setDefault(this.allZonesEnabledPrefName_, false);
+ }
+}
+
+/**
+ * @returns Boolean indicating whether call tracing is enabled
+ */
+G_DebugService.prototype.callTracingEnabled = function() {
+ if (G_GDEBUG) {
+ return this.settings_.getSetting(this.callTracingEnabledPrefName_, false);
+ }
+}
+
+/**
+ * Enables call tracing
+ */
+G_DebugService.prototype.enableCallTracing = function() {
+ if (G_GDEBUG) {
+ this.settings_.setDefault(this.callTracingEnabledPrefName_, true);
+ }
+}
+
+/**
+ * Disables call tracing
+ */
+G_DebugService.prototype.disableCallTracing = function() {
+ if (G_GDEBUG) {
+ this.settings_.setDefault(this.callTracingEnabledPrefName_, false);
+ }
+}
+
+/**
+ * Gets the minimum error that will be reported to the log.
+ */
+G_DebugService.prototype.getLogFileErrorLevel = function() {
+ if (G_GDEBUG) {
+ var level = this.settings_.getSetting(this.logFileErrorLevelPrefName_,
+ G_DebugService.ERROR_LEVEL_EXCEPTION);
+
+ return level.toUpperCase();
+ }
+}
+
+/**
+ * Sets the minimum error level that will be reported to the log.
+ */
+G_DebugService.prototype.setLogFileErrorLevel = function(level) {
+ if (G_GDEBUG) {
+ // normalize case just to make it slightly easier to not screw up.
+ level = level.toUpperCase();
+
+ if (level != G_DebugService.ERROR_LEVEL_INFO &&
+ level != G_DebugService.ERROR_LEVEL_WARNING &&
+ level != G_DebugService.ERROR_LEVEL_EXCEPTION) {
+ throw new Error("Invalid error level specified: {" + level + "}");
+ }
+
+ this.settings_.setDefault(this.logFileErrorLevelPrefName_, level);
+ }
+}
+
+/**
+ * Internal dump() method
+ *
+ * @param msg String of message to dump
+ */
+G_DebugService.prototype.dump = function(msg) {
+ if (G_GDEBUG) {
+ dump(msg);
+
+ if (this.alsoDumpToConsole()) {
+ try {
+ var console = Components.classes['@mozilla.org/consoleservice;1']
+ .getService(Components.interfaces.nsIConsoleService);
+ console.logStringMessage(msg);
+ } catch(e) {
+ dump("G_DebugZone ERROR: COULD NOT DUMP TO CONSOLE\n");
+ }
+ }
+
+ this.maybeDumpToFile(msg);
+ }
+}
+
+/**
+ * Writes the specified message to the log file, if file logging is enabled.
+ */
+G_DebugService.prototype.maybeDumpToFile = function(msg) {
+ if (this.logFileIsEnabled() && this.logFile_) {
+
+ /* try to get the correct line end character for this platform */
+ if (!this._LINE_END_CHAR)
+ this._LINE_END_CHAR =
+ Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime)
+ .OS == "WINNT" ? "\r\n" : "\n";
+ if (this._LINE_END_CHAR != "\n")
+ msg = msg.replace(/\n/g, this._LINE_END_CHAR);
+
+ try {
+ var stream = Cc["@mozilla.org/network/file-output-stream;1"]
+ .createInstance(Ci.nsIFileOutputStream);
+ stream.init(this.logFile_,
+ 0x02 | 0x08 | 0x10 /* PR_WRONLY | PR_CREATE_FILE | PR_APPEND */
+ -1 /* default perms */, 0 /* no special behavior */);
+ stream.write(msg, msg.length);
+ } finally {
+ stream.close();
+ }
+ }
+}
+
+/**
+ * Implements nsIConsoleListener.observe(). Gets called when an error message
+ * gets reported to the console and sends it to the log file as well.
+ */
+G_DebugService.prototype.observe = function(consoleMessage) {
+ if (G_GDEBUG) {
+ var errorLevel = this.getLogFileErrorLevel();
+
+ // consoleMessage can be either nsIScriptError or nsIConsoleMessage. The
+ // latter does not have things like line number, etc. So we special case
+ // it first.
+ if (!(consoleMessage instanceof Ci.nsIScriptError)) {
+ // Only report these messages if the error level is INFO.
+ if (errorLevel == G_DebugService.ERROR_LEVEL_INFO) {
+ this.maybeDumpToFile(G_DebugService.ERROR_LEVEL_INFO + ": " +
+ consoleMessage.message + "\n");
+ }
+
+ return;
+ }
+
+ // We make a local copy of these fields because writing to it doesn't seem
+ // to work.
+ var flags = consoleMessage.flags;
+ var sourceName = consoleMessage.sourceName;
+ var lineNumber = consoleMessage.lineNumber;
+
+ // Sometimes, a scripterror instance won't have any flags set. We
+ // default to exception.
+ if (!flags) {
+ flags = Ci.nsIScriptError.exceptionFlag;
+ }
+
+ // Default the filename and line number if they aren't set.
+ if (!sourceName) {
+ sourceName = "<unknown>";
+ }
+
+ if (!lineNumber) {
+ lineNumber = "<unknown>";
+ }
+
+ // Report the error in the log file.
+ if (flags & Ci.nsIScriptError.warningFlag) {
+ // Only report warnings if the error level is warning or better.
+ if (errorLevel == G_DebugService.ERROR_LEVEL_WARNING ||
+ errorLevel == G_DebugService.ERROR_LEVEL_INFO) {
+ this.reportScriptError_(consoleMessage.message,
+ sourceName,
+ lineNumber,
+ G_DebugService.ERROR_LEVEL_WARNING);
+ }
+ } else if (flags & Ci.nsIScriptError.exceptionFlag) {
+ // Always report exceptions.
+ this.reportScriptError_(consoleMessage.message,
+ sourceName,
+ lineNumber,
+ G_DebugService.ERROR_LEVEL_EXCEPTION);
+ }
+ }
+}
+
+/**
+ * Private helper to report an nsIScriptError instance to the log/console.
+ */
+G_DebugService.prototype.reportScriptError_ = function(message, sourceName,
+ lineNumber, label) {
+ message = "\n------------------------------------------------------------\n" +
+ label + ": " + message +
+ "\nlocation: " + sourceName + ", " + "line: " + lineNumber +
+ "\n------------------------------------------------------------\n\n";
+
+ dump(message);
+ this.maybeDumpToFile(message);
+}
+
+
+
+/**
+ * A class that instruments methods so they output a call trace,
+ * including the values of their actual parameters and return value.
+ * This code is mostly stolen from Aaron Boodman's original
+ * implementation in clobber utils.
+ *
+ * Note that this class uses the "loggifier" debug zone, so you'll see
+ * a complete call trace when that zone is enabled.
+ *
+ * @constructor
+ */
+this.G_Loggifier = function G_Loggifier() {
+ if (G_GDEBUG) {
+ // Careful not to loggify ourselves!
+ this.mark_(this);
+ }
+}
+
+/**
+ * Marks an object as having been loggified. Loggification is not
+ * idempotent :)
+ *
+ * @param obj Object to be marked
+ */
+G_Loggifier.prototype.mark_ = function(obj) {
+ if (G_GDEBUG) {
+ obj.__loggified_ = true;
+ }
+}
+
+/**
+ * @param obj Object to be examined
+ * @returns Boolean indicating if the object has been loggified
+ */
+G_Loggifier.prototype.isLoggified = function(obj) {
+ if (G_GDEBUG) {
+ return !!obj.__loggified_;
+ }
+}
+
+/**
+ * Attempt to extract the class name from the constructor definition.
+ * Assumes the object was created using new.
+ *
+ * @param constructor String containing the definition of a constructor,
+ * for example what you'd get by examining obj.constructor
+ * @returns Name of the constructor/object if it could be found, else "???"
+ */
+G_Loggifier.prototype.getFunctionName_ = function(constructor) {
+ if (G_GDEBUG) {
+ return constructor.name || "???";
+ }
+}
+
+/**
+ * Wraps all the methods in an object so that call traces are
+ * automatically outputted.
+ *
+ * @param obj Object to loggify. SHOULD BE THE PROTOTYPE OF A USER-DEFINED
+ * object. You can get into trouble if you attempt to
+ * loggify something that isn't, for example the Window.
+ *
+ * Any additional parameters are considered method names which should not be
+ * loggified.
+ *
+ * Usage:
+ * G_debugService.loggifier.loggify(MyClass.prototype,
+ * "firstMethodNotToLog",
+ * "secondMethodNotToLog",
+ * ... etc ...);
+ */
+G_Loggifier.prototype.loggify = function(obj) {
+ if (G_GDEBUG) {
+ if (!G_debugService.callTracingEnabled()) {
+ return;
+ }
+
+ if (typeof window != "undefined" && obj == window ||
+ this.isLoggified(obj)) // Don't go berserk!
+ return;
+
+ var zone = G_GetDebugZone(obj);
+ if (!zone || !zone.zoneIsEnabled()) {
+ return;
+ }
+
+ this.mark_(obj);
+
+ // Helper function returns an instrumented version of
+ // objName.meth, with "this" bound properly. (BTW, because we're
+ // in a conditional here, functions will only be defined as
+ // they're encountered during execution, so declare this helper
+ // before using it.)
+
+ let wrap = function (meth, objName, methName) {
+ return function() {
+
+ // First output the call along with actual parameters
+ var args = new Array(arguments.length);
+ var argsString = "";
+ for (var i = 0; i < args.length; i++) {
+ args[i] = arguments[i];
+ argsString += (i == 0 ? "" : ", ");
+
+ if (typeof args[i] == "function") {
+ argsString += "[function]";
+ } else {
+ argsString += args[i];
+ }
+ }
+
+ G_TraceCall(this, "> " + objName + "." + methName + "(" +
+ argsString + ")");
+
+ // Then run the function, capturing the return value and throws
+ try {
+ var retVal = meth.apply(this, arguments);
+ var reportedRetVal = retVal;
+
+ if (typeof reportedRetVal == "undefined")
+ reportedRetVal = "void";
+ else if (reportedRetVal === "")
+ reportedRetVal = "\"\" (empty string)";
+ } catch (e) {
+ if (e && !e.__logged) {
+ G_TraceCall(this, "Error: " + e.message + ". " +
+ e.fileName + ": " + e.lineNumber);
+ try {
+ e.__logged = true;
+ } catch (e2) {
+ // Sometimes we can't add the __logged flag because it's an
+ // XPC wrapper
+ throw e;
+ }
+ }
+
+ throw e; // Re-throw!
+ }
+
+ // And spit it out already
+ G_TraceCall(
+ this,
+ "< " + objName + "." + methName + ": " + reportedRetVal);
+
+ return retVal;
+ };
+ };
+
+ var ignoreLookup = {};
+
+ if (arguments.length > 1) {
+ for (var i = 1; i < arguments.length; i++) {
+ ignoreLookup[arguments[i]] = true;
+ }
+ }
+
+ // Wrap each method of obj
+ for (var p in obj) {
+ // Work around bug in Firefox. In ffox typeof RegExp is "function",
+ // so make sure this really is a function. Bug as of FFox 1.5b2.
+ if (typeof obj[p] == "function" && obj[p].call && !ignoreLookup[p]) {
+ var objName = this.getFunctionName_(obj.constructor);
+ obj[p] = wrap(obj[p], objName, p);
+ }
+ }
+ }
+}
+
+
+/**
+ * Simple abstraction around debug settings. The thing with debug settings is
+ * that we want to be able to specify a default in the application's startup,
+ * but have that default be overridable by the user via their prefs.
+ *
+ * To generalize this, we package up a dictionary of defaults with the
+ * preferences tree. If a setting isn't in the preferences tree, then we grab it
+ * from the defaults.
+ */
+this.G_DebugSettings = function G_DebugSettings() {
+ this.defaults_ = {};
+ this.prefs_ = new G_Preferences();
+}
+
+/**
+ * Returns the value of a settings, optionally defaulting to a given value if it
+ * doesn't exist. If no default is specified, the default is |undefined|.
+ */
+G_DebugSettings.prototype.getSetting = function(name, opt_default) {
+ var override = this.prefs_.getPref(name, null);
+
+ if (override !== null) {
+ return override;
+ } else if (typeof this.defaults_[name] != "undefined") {
+ return this.defaults_[name];
+ } else {
+ return opt_default;
+ }
+}
+
+/**
+ * Sets the default value for a setting. If the user doesn't override it with a
+ * preference, this is the value which will be returned by getSetting().
+ */
+G_DebugSettings.prototype.setDefault = function(name, val) {
+ this.defaults_[name] = val;
+}
+
+var G_debugService = new G_DebugService(); // Instantiate us!
+
+if (G_GDEBUG) {
+ G_debugService.enableAllZones();
+}
+
+#else
+
+// Stubs for the debugging aids scattered through this component.
+// They will be expanded if you compile yourself a debug build.
+
+this.G_Debug = function G_Debug(who, msg) { }
+this.G_Assert = function G_Assert(who, condition, msg) { }
+this.G_Error = function G_Error(who, msg) { }
+this.G_debugService = {
+ alsoDumpToConsole: () => {},
+ logFileIsEnabled: () => {},
+ enableLogFile: () => {},
+ disableLogFile: () => {},
+ getLogFile: () => {},
+ setLogFile: () => {},
+ enableDumpToConsole: () => {},
+ disableDumpToConsole: () => {},
+ getZone: () => {},
+ enableZone: () => {},
+ disableZone: () => {},
+ allZonesEnabled: () => {},
+ enableAllZones: () => {},
+ disableAllZones: () => {},
+ callTracingEnabled: () => {},
+ enableCallTracing: () => {},
+ disableCallTracing: () => {},
+ getLogFileErrorLevel: () => {},
+ setLogFileErrorLevel: () => {},
+ dump: () => {},
+ maybeDumpToFile: () => {},
+ observe: () => {},
+ reportScriptError_: () => {}
+};
+
+#endif
diff --git a/toolkit/components/url-classifier/content/moz/lang.js b/toolkit/components/url-classifier/content/moz/lang.js
new file mode 100644
index 000000000..804a6e973
--- /dev/null
+++ b/toolkit/components/url-classifier/content/moz/lang.js
@@ -0,0 +1,82 @@
+# 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/.
+
+
+/**
+ * lang.js - Some missing JavaScript language features
+ */
+
+/**
+ * Partially applies a function to a particular "this object" and zero or
+ * more arguments. The result is a new function with some arguments of the first
+ * function pre-filled and the value of |this| "pre-specified".
+ *
+ * Remaining arguments specified at call-time are appended to the pre-
+ * specified ones.
+ *
+ * Usage:
+ * var barMethBound = BindToObject(myFunction, myObj, "arg1", "arg2");
+ * barMethBound("arg3", "arg4");
+ *
+ * @param fn {string} Reference to the function to be bound
+ *
+ * @param self {object} Specifies the object which |this| should point to
+ * when the function is run. If the value is null or undefined, it will default
+ * to the global object.
+ *
+ * @returns {function} A partially-applied form of the speficied function.
+ */
+this.BindToObject = function BindToObject(fn, self, opt_args) {
+ var boundargs = fn.boundArgs_ || [];
+ boundargs = boundargs.concat(Array.slice(arguments, 2, arguments.length));
+
+ if (fn.boundSelf_)
+ self = fn.boundSelf_;
+ if (fn.boundFn_)
+ fn = fn.boundFn_;
+
+ var newfn = function() {
+ // Combine the static args and the new args into one big array
+ var args = boundargs.concat(Array.slice(arguments));
+ return fn.apply(self, args);
+ }
+
+ newfn.boundArgs_ = boundargs;
+ newfn.boundSelf_ = self;
+ newfn.boundFn_ = fn;
+
+ return newfn;
+}
+
+/**
+ * Inherit the prototype methods from one constructor into another.
+ *
+ * Usage:
+ *
+ * function ParentClass(a, b) { }
+ * ParentClass.prototype.foo = function(a) { }
+ *
+ * function ChildClass(a, b, c) {
+ * ParentClass.call(this, a, b);
+ * }
+ *
+ * ChildClass.inherits(ParentClass);
+ *
+ * var child = new ChildClass("a", "b", "see");
+ * child.foo(); // works
+ *
+ * In addition, a superclass' implementation of a method can be invoked
+ * as follows:
+ *
+ * ChildClass.prototype.foo = function(a) {
+ * ChildClass.superClass_.foo.call(this, a);
+ * // other code
+ * };
+ */
+Function.prototype.inherits = function(parentCtor) {
+ var tempCtor = function(){};
+ tempCtor.prototype = parentCtor.prototype;
+ this.superClass_ = parentCtor.prototype;
+ this.prototype = new tempCtor();
+}
diff --git a/toolkit/components/url-classifier/content/moz/observer.js b/toolkit/components/url-classifier/content/moz/observer.js
new file mode 100644
index 000000000..a9d22ee21
--- /dev/null
+++ b/toolkit/components/url-classifier/content/moz/observer.js
@@ -0,0 +1,145 @@
+# 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/.
+
+
+// A couple of classes to simplify creating observers.
+//
+// Example1:
+//
+// function doSomething() { ... }
+// var observer = new G_ObserverWrapper(topic, doSomething);
+// someObj.addObserver(topic, observer);
+//
+// Example2:
+//
+// function doSomething() { ... }
+// new G_ObserverServiceObserver("profile-after-change",
+// doSomething,
+// true /* run only once */);
+
+
+/**
+ * This class abstracts the admittedly simple boilerplate required of
+ * an nsIObserver. It saves you the trouble of implementing the
+ * indirection of your own observe() function.
+ *
+ * @param topic String containing the topic the observer will filter for
+ *
+ * @param observeFunction Reference to the function to call when the
+ * observer fires
+ *
+ * @constructor
+ */
+this.G_ObserverWrapper = function G_ObserverWrapper(topic, observeFunction) {
+ this.debugZone = "observer";
+ this.topic_ = topic;
+ this.observeFunction_ = observeFunction;
+}
+
+/**
+ * XPCOM
+ */
+G_ObserverWrapper.prototype.QueryInterface = function(iid) {
+ if (iid.equals(Ci.nsISupports) || iid.equals(Ci.nsIObserver))
+ return this;
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+}
+
+/**
+ * Invoked by the thingy being observed
+ */
+G_ObserverWrapper.prototype.observe = function(subject, topic, data) {
+ if (topic == this.topic_)
+ this.observeFunction_(subject, topic, data);
+}
+
+
+/**
+ * This class abstracts the admittedly simple boilerplate required of
+ * observing an observerservice topic. It implements the indirection
+ * required, and automatically registers to hear the topic.
+ *
+ * @param topic String containing the topic the observer will filter for
+ *
+ * @param observeFunction Reference to the function to call when the
+ * observer fires
+ *
+ * @param opt_onlyOnce Boolean indicating if the observer should unregister
+ * after it has fired
+ *
+ * @constructor
+ */
+this.G_ObserverServiceObserver =
+function G_ObserverServiceObserver(topic, observeFunction, opt_onlyOnce) {
+ this.debugZone = "observerserviceobserver";
+ this.topic_ = topic;
+ this.observeFunction_ = observeFunction;
+ this.onlyOnce_ = !!opt_onlyOnce;
+
+ this.observer_ = new G_ObserverWrapper(this.topic_,
+ BindToObject(this.observe_, this));
+ this.observerService_ = Cc["@mozilla.org/observer-service;1"]
+ .getService(Ci.nsIObserverService);
+ this.observerService_.addObserver(this.observer_, this.topic_, false);
+}
+
+/**
+ * Unregister the observer from the observerservice
+ */
+G_ObserverServiceObserver.prototype.unregister = function() {
+ this.observerService_.removeObserver(this.observer_, this.topic_);
+ this.observerService_ = null;
+}
+
+/**
+ * Invoked by the observerservice
+ */
+G_ObserverServiceObserver.prototype.observe_ = function(subject, topic, data) {
+ this.observeFunction_(subject, topic, data);
+ if (this.onlyOnce_)
+ this.unregister();
+}
+
+#ifdef DEBUG
+this.TEST_G_Observer = function TEST_G_Observer() {
+ if (G_GDEBUG) {
+
+ var z = "observer UNITTEST";
+ G_debugService.enableZone(z);
+
+ G_Debug(z, "Starting");
+
+ var regularObserverRan = 0;
+ var observerServiceObserverRan = 0;
+
+ let regularObserver = function () {
+ regularObserverRan++;
+ };
+
+ let observerServiceObserver = function () {
+ observerServiceObserverRan++;
+ };
+
+ var service = Cc["@mozilla.org/observer-service;1"]
+ .getService(Ci.nsIObserverService);
+ var topic = "google-observer-test";
+
+ var o1 = new G_ObserverWrapper(topic, regularObserver);
+ service.addObserver(o1, topic, false);
+
+ new G_ObserverServiceObserver(topic,
+ observerServiceObserver, true /* once */);
+
+ // Notifications happen synchronously, so this is easy
+ service.notifyObservers(null, topic, null);
+ service.notifyObservers(null, topic, null);
+
+ G_Assert(z, regularObserverRan == 2, "Regular observer broken");
+ G_Assert(z, observerServiceObserverRan == 1, "ObsServObs broken");
+
+ service.removeObserver(o1, topic);
+ G_Debug(z, "PASSED");
+ }
+}
+#endif
diff --git a/toolkit/components/url-classifier/content/moz/preferences.js b/toolkit/components/url-classifier/content/moz/preferences.js
new file mode 100644
index 000000000..30105ab34
--- /dev/null
+++ b/toolkit/components/url-classifier/content/moz/preferences.js
@@ -0,0 +1,276 @@
+# 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/.
+
+
+// Class for manipulating preferences. Aside from wrapping the pref
+// service, useful functionality includes:
+//
+// - abstracting prefobserving so that you can observe preferences
+// without implementing nsIObserver
+//
+// - getters that return a default value when the pref doesn't exist
+// (instead of throwing)
+//
+// - get-and-set getters
+//
+// Example:
+//
+// var p = new PROT_Preferences();
+// dump(p.getPref("some-true-pref")); // shows true
+// dump(p.getPref("no-such-pref", true)); // shows true
+// dump(p.getPref("no-such-pref", null)); // shows null
+//
+// function observe(prefThatChanged) {
+// dump("Pref changed: " + prefThatChanged);
+// };
+//
+// p.addObserver("somepref", observe);
+// p.setPref("somepref", true); // dumps
+// p.removeObserver("somepref", observe);
+//
+// TODO: should probably have the prefobserver pass in the new and old
+// values
+
+// TODO(tc): Maybe remove this class and just call natively since we're no
+// longer an extension.
+
+/**
+ * A class that wraps the preferences service.
+ *
+ * @param opt_startPoint A starting point on the prefs tree to resolve
+ * names passed to setPref and getPref.
+ *
+ * @param opt_useDefaultPranch Set to true to work against the default
+ * preferences tree instead of the profile one.
+ *
+ * @constructor
+ */
+this.G_Preferences =
+function G_Preferences(opt_startPoint, opt_getDefaultBranch) {
+ this.debugZone = "prefs";
+ this.observers_ = {};
+ this.getDefaultBranch_ = !!opt_getDefaultBranch;
+
+ this.startPoint_ = opt_startPoint || null;
+}
+
+G_Preferences.setterMap_ = { "string": "setCharPref",
+ "boolean": "setBoolPref",
+ "number": "setIntPref" };
+
+G_Preferences.getterMap_ = {};
+G_Preferences.getterMap_[Ci.nsIPrefBranch.PREF_STRING] = "getCharPref";
+G_Preferences.getterMap_[Ci.nsIPrefBranch.PREF_BOOL] = "getBoolPref";
+G_Preferences.getterMap_[Ci.nsIPrefBranch.PREF_INT] = "getIntPref";
+
+G_Preferences.prototype.__defineGetter__('prefs_', function() {
+ var prefs;
+ var prefSvc = Cc["@mozilla.org/preferences-service;1"]
+ .getService(Ci.nsIPrefService);
+
+ if (this.getDefaultBranch_) {
+ prefs = prefSvc.getDefaultBranch(this.startPoint_);
+ } else {
+ prefs = prefSvc.getBranch(this.startPoint_);
+ }
+
+ // QI to prefs in case we want to add observers
+ prefs.QueryInterface(Ci.nsIPrefBranchInternal);
+ return prefs;
+});
+
+/**
+ * Stores a key/value in a user preference. Valid types for val are string,
+ * boolean, and number. Complex values are not yet supported (but feel free to
+ * add them!).
+ */
+G_Preferences.prototype.setPref = function(key, val) {
+ var datatype = typeof(val);
+
+ if (datatype == "number" && (val % 1 != 0)) {
+ throw new Error("Cannot store non-integer numbers in preferences.");
+ }
+
+ var meth = G_Preferences.setterMap_[datatype];
+
+ if (!meth) {
+ throw new Error("Pref datatype {" + datatype + "} not supported.");
+ }
+
+ return this.prefs_[meth](key, val);
+}
+
+/**
+ * Retrieves a user preference. Valid types for the value are the same as for
+ * setPref. If the preference is not found, opt_default will be returned
+ * instead.
+ */
+G_Preferences.prototype.getPref = function(key, opt_default) {
+ var type = this.prefs_.getPrefType(key);
+
+ // zero means that the specified pref didn't exist
+ if (type == Ci.nsIPrefBranch.PREF_INVALID) {
+ return opt_default;
+ }
+
+ var meth = G_Preferences.getterMap_[type];
+
+ if (!meth) {
+ throw new Error("Pref datatype {" + type + "} not supported.");
+ }
+
+ // If a pref has been cleared, it will have a valid type but won't
+ // be gettable, so this will throw.
+ try {
+ return this.prefs_[meth](key);
+ } catch(e) {
+ return opt_default;
+ }
+}
+
+/**
+ * Delete a preference.
+ *
+ * @param which Name of preference to obliterate
+ */
+G_Preferences.prototype.clearPref = function(which) {
+ try {
+ // This throws if the pref doesn't exist, which is fine because a
+ // nonexistent pref is cleared
+ this.prefs_.clearUserPref(which);
+ } catch(e) {}
+}
+
+/**
+ * Add an observer for a given pref.
+ *
+ * @param which String containing the pref to listen to
+ * @param callback Function to be called when the pref changes. This
+ * function will receive a single argument, a string
+ * holding the preference name that changed
+ */
+G_Preferences.prototype.addObserver = function(which, callback) {
+ // Need to store the observer we create so we can eventually unregister it
+ if (!this.observers_[which])
+ this.observers_[which] = { callbacks: [], observers: [] };
+
+ /* only add an observer if the callback hasn't been registered yet */
+ if (this.observers_[which].callbacks.indexOf(callback) == -1) {
+ var observer = new G_PreferenceObserver(callback);
+ this.observers_[which].callbacks.push(callback);
+ this.observers_[which].observers.push(observer);
+ this.prefs_.addObserver(which, observer, false /* strong reference */);
+ }
+}
+
+/**
+ * Remove an observer for a given pref.
+ *
+ * @param which String containing the pref to stop listening to
+ * @param callback Function to remove as an observer
+ */
+G_Preferences.prototype.removeObserver = function(which, callback) {
+ var ix = this.observers_[which].callbacks.indexOf(callback);
+ G_Assert(this, ix != -1, "Tried to unregister a nonexistent observer");
+ this.observers_[which].callbacks.splice(ix, 1);
+ var observer = this.observers_[which].observers.splice(ix, 1)[0];
+ this.prefs_.removeObserver(which, observer);
+}
+
+/**
+ * Remove all preference observers registered through this object.
+ */
+G_Preferences.prototype.removeAllObservers = function() {
+ for (var which in this.observers_) {
+ for (var observer of this.observers_[which].observers) {
+ this.prefs_.removeObserver(which, observer);
+ }
+ }
+ this.observers_ = {};
+}
+
+/**
+ * Helper class that knows how to observe preference changes and
+ * invoke a callback when they do
+ *
+ * @constructor
+ * @param callback Function to call when the preference changes
+ */
+this.G_PreferenceObserver =
+function G_PreferenceObserver(callback) {
+ this.debugZone = "prefobserver";
+ this.callback_ = callback;
+}
+
+/**
+ * Invoked by the pref system when a preference changes. Passes the
+ * message along to the callback.
+ *
+ * @param subject The nsIPrefBranch that changed
+ * @param topic String "nsPref:changed" (aka
+ * NS_PREFBRANCH_PREFCHANGE_OBSERVER_ID -- but where does it
+ * live???)
+ * @param data Name of the pref that changed
+ */
+G_PreferenceObserver.prototype.observe = function(subject, topic, data) {
+ G_Debug(this, "Observed pref change: " + data);
+ this.callback_(data);
+}
+
+/**
+ * XPCOM cruft
+ *
+ * @param iid Interface id of the interface the caller wants
+ */
+G_PreferenceObserver.prototype.QueryInterface = function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIObserver) ||
+ iid.equals(Ci.nsISupportsWeakReference))
+ return this;
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+}
+
+#ifdef DEBUG
+// UNITTESTS
+this.TEST_G_Preferences = function TEST_G_Preferences() {
+ if (G_GDEBUG) {
+ var z = "preferences UNITTEST";
+ G_debugService.enableZone(z);
+ G_Debug(z, "Starting");
+
+ var p = new G_Preferences();
+
+ var testPref = "test-preferences-unittest";
+ var noSuchPref = "test-preferences-unittest-aypabtu";
+
+ // Used to test observing
+ var observeCount = 0;
+ let observe = function (prefChanged) {
+ G_Assert(z, prefChanged == testPref, "observer broken");
+ observeCount++;
+ };
+
+ // Test setting, getting, and observing
+ p.addObserver(testPref, observe);
+ p.setPref(testPref, true);
+ G_Assert(z, p.getPref(testPref), "get or set broken");
+ G_Assert(z, observeCount == 1, "observer adding not working");
+
+ p.removeObserver(testPref, observe);
+
+ p.setPref(testPref, false);
+ G_Assert(z, observeCount == 1, "observer removal not working");
+ G_Assert(z, !p.getPref(testPref), "get broken");
+
+ // Remember to clean up the prefs we've set, and test removing prefs
+ // while we're at it
+ p.clearPref(noSuchPref);
+ G_Assert(z, !p.getPref(noSuchPref, false), "clear broken");
+
+ p.clearPref(testPref);
+
+ G_Debug(z, "PASSED");
+ }
+}
+#endif
diff --git a/toolkit/components/url-classifier/content/moz/protocol4.js b/toolkit/components/url-classifier/content/moz/protocol4.js
new file mode 100644
index 000000000..a75f6b531
--- /dev/null
+++ b/toolkit/components/url-classifier/content/moz/protocol4.js
@@ -0,0 +1,133 @@
+# 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/.
+
+
+// A helper class that knows how to parse from and serialize to
+// protocol4. This is a simple, historical format used by some Google
+// interfaces, for example the Toolbar (i.e., ancient services).
+//
+// Protocol4 consists of a newline-separated sequence of name/value
+// pairs (strings). Each line consists of the name, the value length,
+// and the value itself, all separated by colons. Example:
+//
+// foo:6:barbaz\n
+// fritz:33:issickofdynamicallytypedlanguages\n
+
+
+/**
+ * This class knows how to serialize/deserialize maps to/from their
+ * protocol4 representation.
+ *
+ * @constructor
+ */
+this.G_Protocol4Parser = function G_Protocol4Parser() {
+ this.debugZone = "protocol4";
+
+ this.protocol4RegExp_ = new RegExp("([^:]+):\\d+:(.*)$");
+ this.newlineRegExp_ = new RegExp("(\\r)?\\n");
+}
+
+/**
+ * Create a map from a protocol4 string. Silently skips invalid lines.
+ *
+ * @param text String holding the protocol4 representation
+ *
+ * @returns Object as an associative array with keys and values
+ * given in text. The empty object is returned if none
+ * are parsed.
+ */
+G_Protocol4Parser.prototype.parse = function(text) {
+
+ var response = {};
+ if (!text)
+ return response;
+
+ // Responses are protocol4: (repeated) name:numcontentbytes:content\n
+ var lines = text.split(this.newlineRegExp_);
+ for (var i = 0; i < lines.length; i++)
+ if (this.protocol4RegExp_.exec(lines[i]))
+ response[RegExp.$1] = RegExp.$2;
+
+ return response;
+}
+
+/**
+ * Create a protocol4 string from a map (object). Throws an error on
+ * an invalid input.
+ *
+ * @param map Object as an associative array with keys and values
+ * given as strings.
+ *
+ * @returns text String holding the protocol4 representation
+ */
+G_Protocol4Parser.prototype.serialize = function(map) {
+ if (typeof map != "object")
+ throw new Error("map must be an object");
+
+ var text = "";
+ for (var key in map) {
+ if (typeof map[key] != "string")
+ throw new Error("Keys and values must be strings");
+
+ text += key + ":" + map[key].length + ":" + map[key] + "\n";
+ }
+
+ return text;
+}
+
+#ifdef DEBUG
+/**
+ * Cheesey unittests
+ */
+this.TEST_G_Protocol4Parser = function TEST_G_Protocol4Parser() {
+ if (G_GDEBUG) {
+ var z = "protocol4 UNITTEST";
+ G_debugService.enableZone(z);
+
+ G_Debug(z, "Starting");
+
+ var p = new G_Protocol4Parser();
+
+ let isEmpty = function (map) {
+ for (var key in map)
+ return false;
+ return true;
+ };
+
+ G_Assert(z, isEmpty(p.parse(null)), "Parsing null broken");
+ G_Assert(z, isEmpty(p.parse("")), "Parsing nothing broken");
+
+ var t = "foo:3:bar";
+ G_Assert(z, p.parse(t)["foo"] === "bar", "Parsing one line broken");
+
+ t = "foo:3:bar\n";
+ G_Assert(z, p.parse(t)["foo"] === "bar", "Parsing line with lf broken");
+
+ t = "foo:3:bar\r\n";
+ G_Assert(z, p.parse(t)["foo"] === "bar", "Parsing with crlf broken");
+
+
+ t = "foo:3:bar\nbar:3:baz\r\nbom:3:yaz\n";
+ G_Assert(z, p.parse(t)["foo"] === "bar", "First in multiline");
+ G_Assert(z, p.parse(t)["bar"] === "baz", "Second in multiline");
+ G_Assert(z, p.parse(t)["bom"] === "yaz", "Third in multiline");
+ G_Assert(z, p.parse(t)[""] === undefined, "Nonexistent in multiline");
+
+ // Test serialization
+
+ var original = {
+ "1": "1",
+ "2": "2",
+ "foobar": "baz",
+ "hello there": "how are you?" ,
+ };
+ var deserialized = p.parse(p.serialize(original));
+ for (var key in original)
+ G_Assert(z, original[key] === deserialized[key],
+ "Trouble (de)serializing " + key);
+
+ G_Debug(z, "PASSED");
+ }
+}
+#endif
diff --git a/toolkit/components/url-classifier/content/multi-querier.js b/toolkit/components/url-classifier/content/multi-querier.js
new file mode 100644
index 000000000..f79db8154
--- /dev/null
+++ b/toolkit/components/url-classifier/content/multi-querier.js
@@ -0,0 +1,137 @@
+# 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/.
+
+/**
+ * This class helps us batch a series of async calls to the db.
+ * If any of the tokens is in the database, we fire callback with
+ * true as a param. If all the tokens are not in the database,
+ * we fire callback with false as a param.
+ * This is an "Abstract" base class. Subclasses need to supply
+ * the condition_ method.
+ *
+ * @param tokens Array of strings to lookup in the db
+ * @param tableName String name of the table
+ * @param callback Function callback function that takes true if the condition
+ * passes.
+ */
+this.MultiQuerier =
+function MultiQuerier(tokens, tableName, callback) {
+ this.tokens_ = tokens;
+ this.tableName_ = tableName;
+ this.callback_ = callback;
+ this.dbservice_ = Cc["@mozilla.org/url-classifier/dbservice;1"]
+ .getService(Ci.nsIUrlClassifierDBService);
+ // We put the current token in this variable.
+ this.key_ = null;
+}
+
+/**
+ * Run the remaining tokens against the db.
+ */
+MultiQuerier.prototype.run = function() {
+ if (this.tokens_.length == 0) {
+ this.callback_.handleEvent(false);
+ this.dbservice_ = null;
+ this.callback_ = null;
+ return;
+ }
+
+ this.key_ = this.tokens_.pop();
+ G_Debug(this, "Looking up " + this.key_ + " in " + this.tableName_);
+ this.dbservice_.exists(this.tableName_, this.key_,
+ BindToObject(this.result_, this));
+}
+
+/**
+ * Callback from the db. If the returned value passes the this.condition_
+ * test, go ahead and call the main callback.
+ */
+MultiQuerier.prototype.result_ = function(value) {
+ if (this.condition_(value)) {
+ this.callback_.handleEvent(true)
+ this.dbservice_ = null;
+ this.callback_ = null;
+ } else {
+ this.run();
+ }
+}
+
+// Subclasses must override this.
+MultiQuerier.prototype.condition_ = function(value) {
+ throw "MultiQuerier is an abstract base class";
+}
+
+
+/**
+ * Concrete MultiQuerier that stops if the key exists in the db.
+ */
+this.ExistsMultiQuerier =
+function ExistsMultiQuerier(tokens, tableName, callback) {
+ MultiQuerier.call(this, tokens, tableName, callback);
+ this.debugZone = "existsMultiQuerier";
+}
+
+ExistsMultiQuerier.inherits = function(parentCtor) {
+ var tempCtor = function(){};
+ tempCtor.prototype = parentCtor.prototype;
+ this.superClass_ = parentCtor.prototype;
+ this.prototype = new tempCtor();
+}
+ExistsMultiQuerier.inherits(MultiQuerier);
+
+ExistsMultiQuerier.prototype.condition_ = function(value) {
+ return value.length > 0;
+}
+
+
+/**
+ * Concrete MultiQuerier that looks up a key, decrypts it, then
+ * checks the the resulting regular expressions for a match.
+ * @param tokens Array of hosts
+ */
+this.EnchashMultiQuerier =
+function EnchashMultiQuerier(tokens, tableName, callback, url) {
+ MultiQuerier.call(this, tokens, tableName, callback);
+ this.url_ = url;
+ this.enchashDecrypter_ = new PROT_EnchashDecrypter();
+ this.debugZone = "enchashMultiQuerier";
+}
+
+EnchashMultiQuerier.inherits = function(parentCtor) {
+ var tempCtor = function(){};
+ tempCtor.prototype = parentCtor.prototype;
+ this.superClass_ = parentCtor.prototype;
+ this.prototype = new tempCtor();
+}
+EnchashMultiQuerier.inherits(MultiQuerier);
+
+EnchashMultiQuerier.prototype.run = function() {
+ if (this.tokens_.length == 0) {
+ this.callback_.handleEvent(false);
+ this.dbservice_ = null;
+ this.callback_ = null;
+ return;
+ }
+ var host = this.tokens_.pop();
+ this.key_ = host;
+ var lookupKey = this.enchashDecrypter_.getLookupKey(host);
+ this.dbservice_.exists(this.tableName_, lookupKey,
+ BindToObject(this.result_, this));
+}
+
+EnchashMultiQuerier.prototype.condition_ = function(encryptedValue) {
+ if (encryptedValue.length > 0) {
+ // We have encrypted regular expressions for this host. Let's
+ // decrypt them and see if we have a match.
+ var decrypted = this.enchashDecrypter_.decryptData(encryptedValue,
+ this.key_);
+ var res = this.enchashDecrypter_.parseRegExps(decrypted);
+ for (var j = 0; j < res.length; j++) {
+ if (res[j].test(this.url_)) {
+ return true;
+ }
+ }
+ }
+ return false;
+}
diff --git a/toolkit/components/url-classifier/content/request-backoff.js b/toolkit/components/url-classifier/content/request-backoff.js
new file mode 100644
index 000000000..17e815cf1
--- /dev/null
+++ b/toolkit/components/url-classifier/content/request-backoff.js
@@ -0,0 +1,116 @@
+/* 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/. */
+
+// This implements logic for stopping requests if the server starts to return
+// too many errors. If we get MAX_ERRORS errors in ERROR_PERIOD minutes, we
+// back off for TIMEOUT_INCREMENT minutes. If we get another error
+// immediately after we restart, we double the timeout and add
+// TIMEOUT_INCREMENT minutes, etc.
+//
+// This is similar to the logic used by the search suggestion service.
+
+// HTTP responses that count as an error. We also include any 5xx response
+// as an error.
+this.HTTP_FOUND = 302;
+this.HTTP_SEE_OTHER = 303;
+this.HTTP_TEMPORARY_REDIRECT = 307;
+
+/**
+ * @param maxErrors Number of times to request before backing off.
+ * @param retryIncrement Time (ms) for each retry before backing off.
+ * @param maxRequests Number the number of requests needed to trigger backoff
+ * @param requestPeriod Number time (ms) in which maxRequests have to occur to
+ * trigger the backoff behavior (0 to disable maxRequests)
+ * @param timeoutIncrement Number time (ms) the starting timeout period
+ * we double this time for consecutive errors
+ * @param maxTimeout Number time (ms) maximum timeout period
+ */
+this.RequestBackoff =
+function RequestBackoff(maxErrors, retryIncrement,
+ maxRequests, requestPeriod,
+ timeoutIncrement, maxTimeout) {
+ this.MAX_ERRORS_ = maxErrors;
+ this.RETRY_INCREMENT_ = retryIncrement;
+ this.MAX_REQUESTS_ = maxRequests;
+ this.REQUEST_PERIOD_ = requestPeriod;
+ this.TIMEOUT_INCREMENT_ = timeoutIncrement;
+ this.MAX_TIMEOUT_ = maxTimeout;
+
+ // Queue of ints keeping the time of all requests
+ this.requestTimes_ = [];
+
+ this.numErrors_ = 0;
+ this.errorTimeout_ = 0;
+ this.nextRequestTime_ = 0;
+}
+
+/**
+ * Reset the object for reuse. This deliberately doesn't clear requestTimes_.
+ */
+RequestBackoff.prototype.reset = function() {
+ this.numErrors_ = 0;
+ this.errorTimeout_ = 0;
+ this.nextRequestTime_ = 0;
+}
+
+/**
+ * Check to see if we can make a request.
+ */
+RequestBackoff.prototype.canMakeRequest = function() {
+ var now = Date.now();
+ if (now < this.nextRequestTime_) {
+ return false;
+ }
+
+ return (this.requestTimes_.length < this.MAX_REQUESTS_ ||
+ (now - this.requestTimes_[0]) > this.REQUEST_PERIOD_);
+}
+
+RequestBackoff.prototype.noteRequest = function() {
+ var now = Date.now();
+ this.requestTimes_.push(now);
+
+ // We only care about keeping track of MAX_REQUESTS
+ if (this.requestTimes_.length > this.MAX_REQUESTS_)
+ this.requestTimes_.shift();
+}
+
+RequestBackoff.prototype.nextRequestDelay = function() {
+ return Math.max(0, this.nextRequestTime_ - Date.now());
+}
+
+/**
+ * Notify this object of the last server response. If it's an error,
+ */
+RequestBackoff.prototype.noteServerResponse = function(status) {
+ if (this.isErrorStatus(status)) {
+ this.numErrors_++;
+
+ if (this.numErrors_ < this.MAX_ERRORS_)
+ this.errorTimeout_ = this.RETRY_INCREMENT_;
+ else if (this.numErrors_ == this.MAX_ERRORS_)
+ this.errorTimeout_ = this.TIMEOUT_INCREMENT_;
+ else
+ this.errorTimeout_ *= 2;
+
+ this.errorTimeout_ = Math.min(this.errorTimeout_, this.MAX_TIMEOUT_);
+ this.nextRequestTime_ = Date.now() + this.errorTimeout_;
+ } else {
+ // Reset error timeout, allow requests to go through.
+ this.reset();
+ }
+}
+
+/**
+ * We consider 302, 303, 307, 4xx, and 5xx http responses to be errors.
+ * @param status Number http status
+ * @return Boolean true if we consider this http status an error
+ */
+RequestBackoff.prototype.isErrorStatus = function(status) {
+ return ((400 <= status && status <= 599) ||
+ HTTP_FOUND == status ||
+ HTTP_SEE_OTHER == status ||
+ HTTP_TEMPORARY_REDIRECT == status);
+}
+
diff --git a/toolkit/components/url-classifier/content/trtable.js b/toolkit/components/url-classifier/content/trtable.js
new file mode 100644
index 000000000..c58a80c9a
--- /dev/null
+++ b/toolkit/components/url-classifier/content/trtable.js
@@ -0,0 +1,169 @@
+# 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/.
+
+// XXX: This should all be moved into the dbservice class so it happens
+// in the background thread.
+
+/**
+ * Abstract base class for a lookup table.
+ * @construction
+ */
+this.UrlClassifierTable = function UrlClassifierTable() {
+ this.debugZone = "urlclassifier-table";
+ this.name = '';
+ this.needsUpdate = false;
+ this.enchashDecrypter_ = new PROT_EnchashDecrypter();
+ this.wrappedJSObject = this;
+}
+
+UrlClassifierTable.prototype.QueryInterface = function(iid) {
+ if (iid.equals(Components.interfaces.nsISupports) ||
+ iid.equals(Components.interfaces.nsIUrlClassifierTable))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+}
+
+/**
+ * Subclasses need to implement this method.
+ */
+UrlClassifierTable.prototype.exists = function(url, callback) {
+ throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/////////////////////////////////////////////////////////////////////
+// Url table implementation
+this.UrlClassifierTableUrl = function UrlClassifierTableUrl() {
+ UrlClassifierTable.call(this);
+}
+
+UrlClassifierTableUrl.inherits = function(parentCtor) {
+ var tempCtor = function(){};
+ tempCtor.prototype = parentCtor.prototype;
+ this.superClass_ = parentCtor.prototype;
+ this.prototype = new tempCtor();
+}
+UrlClassifierTableUrl.inherits(UrlClassifierTable);
+
+/**
+ * Look up a URL in a URL table
+ */
+UrlClassifierTableUrl.prototype.exists = function(url, callback) {
+ // nsIUrlClassifierUtils.canonicalizeURL is the old way of canonicalizing a
+ // URL. Unfortunately, it doesn't normalize numeric domains so alternate IP
+ // formats (hex, octal, etc) won't trigger a match.
+ // this.enchashDecrypter_.getCanonicalUrl does the right thing and
+ // normalizes a URL to 4 decimal numbers, but the update server may still be
+ // giving us encoded IP addresses. So to be safe, we check both cases.
+ var urlUtils = Cc["@mozilla.org/url-classifier/utils;1"]
+ .getService(Ci.nsIUrlClassifierUtils);
+ var oldCanonicalized = urlUtils.canonicalizeURL(url);
+ var canonicalized = this.enchashDecrypter_.getCanonicalUrl(url);
+ G_Debug(this, "Looking up: " + url + " (" + oldCanonicalized + " and " +
+ canonicalized + ")");
+ (new ExistsMultiQuerier([oldCanonicalized, canonicalized],
+ this.name,
+ callback)).run();
+}
+
+/////////////////////////////////////////////////////////////////////
+// Domain table implementation
+
+this.UrlClassifierTableDomain = function UrlClassifierTableDomain() {
+ UrlClassifierTable.call(this);
+ this.debugZone = "urlclassifier-table-domain";
+ this.ioService_ = Cc["@mozilla.org/network/io-service;1"]
+ .getService(Ci.nsIIOService);
+}
+
+UrlClassifierTableDomain.inherits = function(parentCtor) {
+ var tempCtor = function(){};
+ tempCtor.prototype = parentCtor.prototype;
+ this.superClass_ = parentCtor.prototype;
+ this.prototype = new tempCtor();
+}
+UrlClassifierTableDomain.inherits(UrlClassifierTable);
+
+/**
+ * Look up a URL in a domain table
+ * We also try to lookup domain + first path component (e.g.,
+ * www.mozilla.org/products).
+ *
+ * @returns Boolean true if the url domain is in the table
+ */
+UrlClassifierTableDomain.prototype.exists = function(url, callback) {
+ var canonicalized = this.enchashDecrypter_.getCanonicalUrl(url);
+ var urlObj = this.ioService_.newURI(canonicalized, null, null);
+ var host = '';
+ try {
+ host = urlObj.host;
+ } catch (e) { }
+ var hostComponents = host.split(".");
+
+ // Try to get the path of the URL. Pseudo urls (like wyciwyg:) throw
+ // errors when trying to convert to an nsIURL so we wrap in a try/catch
+ // block.
+ var path = ""
+ try {
+ urlObj.QueryInterface(Ci.nsIURL);
+ path = urlObj.filePath;
+ } catch (e) { }
+
+ var pathComponents = path.split("/");
+
+ // We don't have a good way map from hosts to domains, so we instead try
+ // each possibility. Could probably optimize to start at the second dot?
+ var possible = [];
+ for (var i = 0; i < hostComponents.length - 1; i++) {
+ host = hostComponents.slice(i).join(".");
+ possible.push(host);
+
+ // The path starts with a "/", so we are interested in the second path
+ // component if it is available
+ if (pathComponents.length >= 2 && pathComponents[1].length > 0) {
+ host = host + "/" + pathComponents[1];
+ possible.push(host);
+ }
+ }
+
+ // Run the possible domains against the db.
+ (new ExistsMultiQuerier(possible, this.name, callback)).run();
+}
+
+/////////////////////////////////////////////////////////////////////
+// Enchash table implementation
+
+this.UrlClassifierTableEnchash = function UrlClassifierTableEnchash() {
+ UrlClassifierTable.call(this);
+ this.debugZone = "urlclassifier-table-enchash";
+}
+
+UrlClassifierTableEnchash.inherits = function(parentCtor) {
+ var tempCtor = function(){};
+ tempCtor.prototype = parentCtor.prototype;
+ this.superClass_ = parentCtor.prototype;
+ this.prototype = new tempCtor();
+}
+UrlClassifierTableEnchash.inherits(UrlClassifierTable);
+
+/**
+ * Look up a URL in an enchashDB. We try all sub domains (up to MAX_DOTS).
+ */
+UrlClassifierTableEnchash.prototype.exists = function(url, callback) {
+ url = this.enchashDecrypter_.getCanonicalUrl(url);
+ var host = this.enchashDecrypter_.getCanonicalHost(url,
+ PROT_EnchashDecrypter.MAX_DOTS);
+
+ var possible = [];
+ for (var i = 0; i < PROT_EnchashDecrypter.MAX_DOTS + 1; i++) {
+ possible.push(host);
+
+ var index = host.indexOf(".");
+ if (index == -1)
+ break;
+ host = host.substring(index + 1);
+ }
+ // Run the possible domains against the db.
+ (new EnchashMultiQuerier(possible, this.name, callback, url)).run();
+}
diff --git a/toolkit/components/url-classifier/content/wireformat.js b/toolkit/components/url-classifier/content/wireformat.js
new file mode 100644
index 000000000..a24b120e6
--- /dev/null
+++ b/toolkit/components/url-classifier/content/wireformat.js
@@ -0,0 +1,230 @@
+# 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/.
+
+
+// A class that serializes and deserializes opaque key/value string to
+// string maps to/from maps (trtables). It knows how to create
+// trtables from the serialized format, so it also understands
+// meta-information like the name of the table and the table's
+// version. See docs for the protocol description.
+//
+// TODO: wireformatreader: if you have multiple updates for one table
+// in a call to deserialize, the later ones will be merged
+// (all but the last will be ignored). To fix, merge instead
+// of replace when you have an existing table, and only do so once.
+// TODO must have blank line between successive types -- problem?
+// TODO doesn't tolerate blank lines very well
+//
+// Maybe: These classes could use a LOT more cleanup, but it's not a
+// priority at the moment. For example, the tablesData/Known
+// maps should be combined into a single object, the parser
+// for a given type should be separate from the version info,
+// and there should be synchronous interfaces for testing.
+
+
+/**
+ * A class that knows how to serialize and deserialize meta-information.
+ * This meta information is the table name and version number, and
+ * in its serialized form looks like the first line below:
+ *
+ * [name-of-table X.Y update?]
+ * ...key/value pairs to add or delete follow...
+ * <blank line ends the table>
+ *
+ * The X.Y is the version number and the optional "update" token means
+ * that the table is a differential from the curent table the extension
+ * has. Its absence means that this is a full, new table.
+ */
+this.PROT_VersionParser =
+function PROT_VersionParser(type, opt_major, opt_minor, opt_requireMac) {
+ this.debugZone = "versionparser";
+ this.type = type;
+ this.major = 0;
+ this.minor = 0;
+
+ this.badHeader = false;
+
+ // Should the wireformatreader compute a mac?
+ this.mac = false;
+ this.macval = "";
+ this.macFailed = false;
+ this.requireMac = !!opt_requireMac;
+
+ this.update = false;
+ this.needsUpdate = false; // used by ListManager to determine update policy
+ // Used by ListerManager to see if we have read data for this table from
+ // disk. Once we read a table from disk, we are not going to do so again
+ // but instead update remotely if necessary.
+ this.didRead = false;
+ if (opt_major)
+ this.major = parseInt(opt_major);
+ if (opt_minor)
+ this.minor = parseInt(opt_minor);
+}
+
+/** Import the version information from another VersionParser
+ * @params version a version parser object
+ */
+PROT_VersionParser.prototype.ImportVersion = function(version) {
+ this.major = version.major;
+ this.minor = version.minor;
+
+ this.mac = version.mac;
+ this.macFailed = version.macFailed;
+ this.macval = version.macval;
+ // Don't set requireMac, since we create vparsers from scratch and doesn't
+ // know about it
+}
+
+/**
+ * Creates a string like [goog-white-black 1.1] from internal information
+ *
+ * @returns String
+ */
+PROT_VersionParser.prototype.toString = function() {
+ var s = "[" + this.type + " " + this.major + "." + this.minor + "]";
+ return s;
+}
+
+/**
+ * Creates a string like 1.123 with the version number. This is the
+ * format we store in prefs.
+ * @return String
+ */
+PROT_VersionParser.prototype.versionString = function() {
+ return this.major + "." + this.minor;
+}
+
+/**
+ * Creates a string like 1:1 from internal information used for
+ * fetching updates from the server. Called by the listmanager.
+ *
+ * @returns String
+ */
+PROT_VersionParser.prototype.toUrl = function() {
+ return this.major + ":" + this.minor;
+}
+
+/**
+ * Process the old format, [type major.minor [update]]
+ *
+ * @returns true if the string could be parsed, false otherwise
+ */
+PROT_VersionParser.prototype.processOldFormat_ = function(line) {
+ if (line[0] != '[' || line.slice(-1) != ']')
+ return false;
+
+ var description = line.slice(1, -1);
+
+ // Get the type name and version number of this table
+ var tokens = description.split(" ");
+ this.type = tokens[0];
+ var majorminor = tokens[1].split(".");
+ this.major = parseInt(majorminor[0]);
+ this.minor = parseInt(majorminor[1]);
+ if (isNaN(this.major) || isNaN(this.minor))
+ return false;
+
+ if (tokens.length >= 3) {
+ this.update = tokens[2] == "update";
+ }
+
+ return true;
+}
+
+/**
+ * Takes a string like [name-of-table 1.1 [update]][mac=MAC] and figures out the
+ * type and corresponding version numbers.
+ * @returns true if the string could be parsed, false otherwise
+ */
+PROT_VersionParser.prototype.fromString = function(line) {
+ G_Debug(this, "Calling fromString with line: " + line);
+ if (line[0] != '[' || line.slice(-1) != ']')
+ return false;
+
+ // There could be two [][], so take care of it
+ var secondBracket = line.indexOf('[', 1);
+ var firstPart = null;
+ var secondPart = null;
+
+ if (secondBracket != -1) {
+ firstPart = line.substring(0, secondBracket);
+ secondPart = line.substring(secondBracket);
+ G_Debug(this, "First part: " + firstPart + " Second part: " + secondPart);
+ } else {
+ firstPart = line;
+ G_Debug(this, "Old format: " + firstPart);
+ }
+
+ if (!this.processOldFormat_(firstPart))
+ return false;
+
+ if (secondPart && !this.processOptTokens_(secondPart))
+ return false;
+
+ return true;
+}
+
+/**
+ * Process optional tokens
+ *
+ * @param line A string [token1=val1 token2=val2...]
+ * @returns true if the string could be parsed, false otherwise
+ */
+PROT_VersionParser.prototype.processOptTokens_ = function(line) {
+ if (line[0] != '[' || line.slice(-1) != ']')
+ return false;
+ var description = line.slice(1, -1);
+ // Get the type name and version number of this table
+ var tokens = description.split(" ");
+
+ for (var i = 0; i < tokens.length; i++) {
+ G_Debug(this, "Processing optional token: " + tokens[i]);
+ var tokenparts = tokens[i].split("=");
+ switch(tokenparts[0]){
+ case "mac":
+ this.mac = true;
+ if (tokenparts.length < 2) {
+ G_Debug(this, "Found mac flag but not mac value!");
+ return false;
+ }
+ // The mac value may have "=" in it, so we can't just use tokenparts[1].
+ // Instead, just take the rest of tokens[i] after the first "="
+ this.macval = tokens[i].substr(tokens[i].indexOf("=")+1);
+ break;
+ default:
+ G_Debug(this, "Found unrecognized token: " + tokenparts[0]);
+ break;
+ }
+ }
+
+ return true;
+}
+
+#ifdef DEBUG
+this.TEST_PROT_WireFormat = function TEST_PROT_WireFormat() {
+ if (G_GDEBUG) {
+ var z = "versionparser UNITTEST";
+ G_Debug(z, "Starting");
+
+ var vp = new PROT_VersionParser("dummy");
+ G_Assert(z, vp.fromString("[foo-bar-url 1.234]"),
+ "failed to parse old format");
+ G_Assert(z, "foo-bar-url" == vp.type, "failed to parse type");
+ G_Assert(z, "1" == vp.major, "failed to parse major");
+ G_Assert(z, "234" == vp.minor, "failed to parse minor");
+
+ vp = new PROT_VersionParser("dummy");
+ G_Assert(z, vp.fromString("[foo-bar-url 1.234][mac=567]"),
+ "failed to parse new format");
+ G_Assert(z, "foo-bar-url" == vp.type, "failed to parse type");
+ G_Assert(z, "1" == vp.major, "failed to parse major");
+ G_Assert(z, "234" == vp.minor, "failed to parse minor");
+ G_Assert(z, true == vp.mac, "failed to parse mac");
+ G_Assert(z, "567" == vp.macval, "failed to parse macval");
+
+ G_Debug(z, "PASSED");
+ }
+}
+#endif
diff --git a/toolkit/components/url-classifier/content/xml-fetcher.js b/toolkit/components/url-classifier/content/xml-fetcher.js
new file mode 100644
index 000000000..39b116e00
--- /dev/null
+++ b/toolkit/components/url-classifier/content/xml-fetcher.js
@@ -0,0 +1,126 @@
+# 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/.
+
+// A simple class that encapsulates a request. You'll notice the
+// style here is different from the rest of the extension; that's
+// because this was re-used from really old code we had. At some
+// point it might be nice to replace this with something better
+// (e.g., something that has explicit onerror handler, ability
+// to set headers, and so on).
+
+/**
+ * Because we might be in a component, we can't just assume that
+ * XMLHttpRequest exists. So we use this tiny factory function to wrap the
+ * XPCOM version.
+ *
+ * @return XMLHttpRequest object
+ */
+this.PROT_NewXMLHttpRequest = function PROT_NewXMLHttpRequest() {
+ var Cc = Components.classes;
+ var Ci = Components.interfaces;
+ var request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
+ .createInstance(Ci.nsIXMLHttpRequest);
+ // Need the following so we get onerror/load/progresschange
+ request.QueryInterface(Ci.nsIJSXMLHttpRequest);
+ return request;
+}
+
+/**
+ * A helper class that does HTTP GETs and calls back a function with
+ * the content it receives. Asynchronous, so uses a closure for the
+ * callback.
+ *
+ * Note, that XMLFetcher is only used for SafeBrowsing, therefore
+ * we inherit from nsILoadContext, so we can use the callbacks on the
+ * channel to separate the safebrowsing cookie based on a reserved
+ * appId.
+ * @constructor
+ */
+this.PROT_XMLFetcher = function PROT_XMLFetcher() {
+ this.debugZone = "xmlfetcher";
+ this._request = PROT_NewXMLHttpRequest();
+ // implements nsILoadContext
+ this.appId = Ci.nsIScriptSecurityManager.SAFEBROWSING_APP_ID;
+ this.isInIsolatedMozBrowserElement = false;
+ this.usePrivateBrowsing = false;
+ this.isContent = false;
+}
+
+PROT_XMLFetcher.prototype = {
+ /**
+ * Function that will be called back upon fetch completion.
+ */
+ _callback: null,
+
+
+ /**
+ * Fetches some content.
+ *
+ * @param page URL to fetch
+ * @param callback Function to call back when complete.
+ */
+ get: function(page, callback) {
+ this._request.abort(); // abort() is asynchronous, so
+ this._request = PROT_NewXMLHttpRequest();
+ this._callback = callback;
+ var asynchronous = true;
+ this._request.loadInfo.originAttributes = {
+ appId: this.appId,
+ inIsolatedMozBrowser: this.isInIsolatedMozBrowserElement
+ };
+ this._request.open("GET", page, asynchronous);
+ this._request.channel.notificationCallbacks = this;
+
+ // Create a closure
+ var self = this;
+ this._request.addEventListener("readystatechange", function() {
+ self.readyStateChange(self);
+ }, false);
+
+ this._request.send(null);
+ },
+
+ cancel: function() {
+ this._request.abort();
+ this._request = null;
+ },
+
+ /**
+ * Called periodically by the request to indicate some state change. 4
+ * means content has been received.
+ */
+ readyStateChange: function(fetcher) {
+ if (fetcher._request.readyState != 4)
+ return;
+
+ // If the request fails, on trunk we get status set to
+ // NS_ERROR_NOT_AVAILABLE. On 1.8.1 branch we get an exception
+ // forwarded from nsIHttpChannel::GetResponseStatus. To be consistent
+ // between branch and trunk, we send back NS_ERROR_NOT_AVAILABLE for
+ // http failures.
+ var responseText = null;
+ var status = Components.results.NS_ERROR_NOT_AVAILABLE;
+ try {
+ G_Debug(this, "xml fetch status code: \"" +
+ fetcher._request.status + "\"");
+ status = fetcher._request.status;
+ responseText = fetcher._request.responseText;
+ } catch(e) {
+ G_Debug(this, "Caught exception trying to read xmlhttprequest " +
+ "status/response.");
+ G_Debug(this, e);
+ }
+ if (fetcher._callback)
+ fetcher._callback(responseText, status);
+ },
+
+ // nsIInterfaceRequestor
+ getInterface: function(iid) {
+ return this.QueryInterface(iid);
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIInterfaceRequestor,
+ Ci.nsISupports,
+ Ci.nsILoadContext])
+};