diff options
Diffstat (limited to 'toolkit/components/url-classifier/content')
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]) +}; |