//@line 37 "M:/central/mozilla/toolkit/components/url-classifier/src/nsUrlClassifierListManager.js"

const Cc = Components.classes;
const Ci = Components.interfaces;

//@line 37 "M:/central/mozilla/toolkit/components/url-classifier/content/listmanager.js"


// 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

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
 */
function PROT_ListManager() {
  this.debugZone = "listmanager";
  G_debugService.enableZone(this.debugZone);

  this.currentUpdateChecker_ = null;   // set when we toggle updates
  this.prefs_ = new G_Preferences();
  this.updateInterval = this.prefs_.getPref("urlclassifier.updateinterval", 30 * 60) * 1000;

  this.updateserverURL_ = null;
  this.gethashURL_ = null;

  this.isTesting_ = false;

  this.tablesData = {};

  this.observerServiceObserver_ = new G_ObserverServiceObserver(
                                          'quit-application',
                                          BindToObject(this.shutdown_, this),
                                          true /*only once*/);

  // Lazily create the key manager (to avoid fetching keys when they
  // aren't needed).
  this.keyManager_ = null;

  this.rekeyObserver_ = new G_ObserverServiceObserver(
                                          'url-classifier-rekey-requested',
                                          BindToObject(this.rekey_, this),
                                          false);
  this.updateWaitingForKey_ = false;

  this.cookieObserver_ = new G_ObserverServiceObserver(
                                          'cookie-changed',
                                          BindToObject(this.cookieChanged_, this),
                                          false);

  /* Backoff interval should be between 30 and 60 minutes. */
  var backoffInterval = 30 * 60 * 1000;
  backoffInterval += Math.floor(Math.random() * (30 * 60 * 1000));

  this.requestBackoff_ = new RequestBackoff(2 /* max errors */,
                                      60*1000 /* retry interval, 1 min */,
                                            4 /* num requests */,
                                   60*60*1000 /* request time, 60 min */,
                              backoffInterval /* backoff interval, 60 min */,
                                 8*60*60*1000 /* max backoff, 8hr */);

  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() {
  if (this.keyManager_) {
    this.keyManager_.shutdown();
  }

  for (var name in this.tablesData) {
    delete this.tablesData[name];
  }
}

/**
 * Set the url we check for updates.  If the new url is valid and different,
 * update our table list.
 * 
 * After setting the update url, the caller is responsible for registering
 * tables and then toggling update checking.  All the code for this logic is
 * currently in browser/components/safebrowsing.  Maybe it should be part of
 * the listmanger?
 */
PROT_ListManager.prototype.setUpdateUrl = function(url) {
  G_Debug(this, "Set update url: " + url);
  if (url != this.updateserverURL_) {
    this.updateserverURL_ = url;
    this.requestBackoff_.reset();
    
    // Remove old tables which probably aren't valid for the new provider.
    for (var name in this.tablesData) {
      delete this.tablesData[name];
    }
  }
}

/**
 * Set the gethash url.
 */
PROT_ListManager.prototype.setGethashUrl = function(url) {
  G_Debug(this, "Set gethash url: " + url);
  if (url != this.gethashURL_) {
    this.gethashURL_ = url;
    this.hashCompleter_.gethashUrl = url;
  }
}

/**
 * Set the crypto key url.
 * @param url String
 */
PROT_ListManager.prototype.setKeyUrl = function(url) {
  G_Debug(this, "Set key url: " + url);
  if (!this.keyManager_) {
    this.keyManager_ = new PROT_UrlCryptoKeyManager();
    this.keyManager_.onNewKey(BindToObject(this.newKey_, this));

    this.hashCompleter_.setKeys(this.keyManager_.getClientKey(),
                                this.keyManager_.getWrappedKey());
  }

  this.keyManager_.setKeyUrl(url);
}

/**
 * Register a new table table
 * @param tableName - the name of the table
 * @param opt_requireMac true if a mac is required on update, false otherwise
 * @returns true if the table could be created; false otherwise
 */
PROT_ListManager.prototype.registerTable = function(tableName, 
                                                    opt_requireMac) {
  this.tablesData[tableName] = {};
  this.tablesData[tableName].needsUpdate = false;

  return true;
}

/**
 * Enable updates for some tables
 * @param tables - an array of table names that need updating
 */
PROT_ListManager.prototype.enableUpdate = function(tableName) {
  var changed = false;
  var table = this.tablesData[tableName];
  if (table) {
    G_Debug(this, "Enabling table updates for " + tableName);
    table.needsUpdate = true;
    changed = true;
  }

  if (changed === true)
    this.maybeToggleUpdateChecking();
}

/**
 * Disables updates for some tables
 * @param tables - an array of table names that no longer need updating
 */
PROT_ListManager.prototype.disableUpdate = function(tableName) {
  var changed = false;
  var table = this.tablesData[tableName];
  if (table) {
    G_Debug(this, "Disabling table updates for " + tableName);
    table.needsUpdate = false;
    changed = true;
  }

  if (changed === true)
    this.maybeToggleUpdateChecking();
}

/**
 * Determine if we have some tables that need updating.
 */
PROT_ListManager.prototype.requireTableUpdates = function() {
  for (var type in this.tablesData) {
    // Tables that need updating even if other tables dont require it
    if (this.tablesData[type].needsUpdate)
      return true;
  }

  return false;
}

/**
 * Start managing the lists we know about. We don't do this automatically
 * when the listmanager is instantiated because their profile directory
 * (where we store the lists) might not be available.
 */
PROT_ListManager.prototype.maybeStartManagingUpdates = function() {
  if (this.isTesting_)
    return;

  // We might have been told about tables already, so see if we should be
  // actually updating.
  this.maybeToggleUpdateChecking();
}

PROT_ListManager.prototype.kickoffUpdate_ = function (tableData)
{
  this.startingUpdate_ = false;
  // If the user has never downloaded tables, do the check now.
  // If the user has tables, add a fuzz of a few minutes.
  var initialUpdateDelay = 3000;
  if (tableData != "") {
    // Add a fuzz of 0-5 minutes.
    initialUpdateDelay += Math.floor(Math.random() * (5 * 60 * 1000));
  }

  this.currentUpdateChecker_ =
    new G_Alarm(BindToObject(this.checkForUpdates, this),
                initialUpdateDelay);
}

/**
 * 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() {
  // If we are testing or dont have an application directory yet, we should
  // not start reading tables from disk or schedule remote updates
  if (this.isTesting_)
    return;

  // 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() === true) {
    G_Debug(this, "Starting managing lists");
    this.startUpdateChecker();

    // Multiple warden can ask us to reenable updates at the same time, but we
    // really just need to schedule a single update.
    if (!this.currentUpdateChecker && !this.startingUpdate_) {
      this.startingUpdate_ = true;
      // check the current state of tables in the database
      this.dbService_.getTables(BindToObject(this.kickoffUpdate_, this));
    }
  } else {
    G_Debug(this, "Stopping managing lists (if currently active)");
    this.stopUpdateChecker();                    // Cancel pending updates
  }
}

/**
 * Start periodic checks for updates. Idempotent.
 * We want to distribute update checks evenly across the update period (an
 * hour).  The first update is scheduled for a random time between 0.5 and 1.5
 * times the update interval.
 */
PROT_ListManager.prototype.startUpdateChecker = function() {
  this.stopUpdateChecker();
  
  // Schedule the first check for between 15 and 45 minutes.
  var repeatingUpdateDelay = this.updateInterval / 2;
  repeatingUpdateDelay += Math.floor(Math.random() * this.updateInterval);
  this.updateChecker_ = new G_Alarm(BindToObject(this.initialUpdateCheck_,
                                                 this),
                                    repeatingUpdateDelay);
}

/**
 * Callback for the first update check.
 * We go ahead and check for table updates, then start a regular timer (once
 * every update interval).
 */
PROT_ListManager.prototype.initialUpdateCheck_ = function() {
  this.checkForUpdates();
  this.updateChecker_ = new G_Alarm(BindToObject(this.checkForUpdates, this), 
                                    this.updateInterval, true /* repeat */);
}

/**
 * Stop checking for updates. Idempotent.
 */
PROT_ListManager.prototype.stopUpdateChecker = function() {
  if (this.updateChecker_) {
    this.updateChecker_.cancel();
    this.updateChecker_ = null;
  }
  // Cancel the oneoff check from maybeToggleUpdateChecking.
  if (this.currentUpdateChecker_) {
    this.currentUpdateChecker_.cancel();
    this.currentUpdateChecker_ = null;
  }
}

/**
 * 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 String Key for table lookup
 * @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 {
    G_Debug(this, "safeLookup: " + key);
    var cb = new QueryAdapter(callback);
    this.dbService_.lookup(key,
                           BindToObject(cb.handleResponse, cb),
                           true);
  } catch(e) {
    G_Debug(this, "safeLookup masked failure for key " + key + ": " + e);
    callback.handleEvent("");
  }
}

/**
 * Updates our internal tables from the update server
 *
 * @returns true when a new request was scheduled, false if an old request
 *          was still pending.
 */
PROT_ListManager.prototype.checkForUpdates = function() {
  // Allow new updates to be scheduled from maybeToggleUpdateChecking()
  this.currentUpdateChecker_ = null;

  if (!this.updateserverURL_) {
    G_Debug(this, 'checkForUpdates: no update server url');
    return false;
  }

  // See if we've triggered the request backoff logic.
  if (!this.requestBackoff_.canMakeRequest())
    return false;

  // Grab the current state of the tables from the database
  this.dbService_.getTables(BindToObject(this.makeUpdateRequest_, this));
  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(tableData) {
  if (!this.keyManager_)
    return;

  if (!this.keyManager_.hasKey()) {
    // We don't have a client key yet.  Schedule a rekey, and rerequest
    // when we have one.

    // If there's already an update waiting for a new key, don't bother.
    if (this.updateWaitingForKey_)
      return;

    // If maybeReKey() returns false we have asked for too many keys,
    // and won't be getting a new one.  Since we don't want to do
    // updates without a client key, we'll skip this update if maybeReKey()
    // fails.
    if (this.keyManager_.maybeReKey())
      this.updateWaitingForKey_ = true;

    return;
  }

  var tableList;
  var tableNames = {};
  for (var tableName in this.tablesData) {
    if (this.tablesData[tableName].needsUpdate)
      tableNames[tableName] = true;
    if (!tableList) {
      tableList = tableName;
    } else {
      tableList += "," + tableName;
    }
  }

  var 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(";");
    if (tableNames[fields[0]]) {
      request += lines[i] + ":mac\n";
      delete tableNames[fields[0]];
    }
  }

  // For each requested table that didn't have chunk data in the database,
  // request it fresh
  for (var tableName in tableNames) {
    request += tableName + ";mac\n";
  }

  G_Debug(this, 'checkForUpdates: scheduling request..');
  var streamer = Cc["@mozilla.org/url-classifier/streamupdater;1"]
                 .getService(Ci.nsIUrlClassifierStreamUpdater);
  try {
    streamer.updateUrl = this.updateserverURL_ +
                         "&wrkey=" + this.keyManager_.getWrappedKey();
  } catch (e) {
    G_Debug(this, 'invalid url');
    return;
  }

  this.requestBackoff_.noteRequest();

  if (!streamer.downloadUpdates(tableList,
                                request,
                                this.keyManager_.getClientKey(),
                                BindToObject(this.updateSuccess_, this),
                                BindToObject(this.updateError_, this),
                                BindToObject(this.downloadError_, this))) {
    G_Debug(this, "pending update, wait 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(waitForUpdate) {
  G_Debug(this, "update success: " + waitForUpdate);
  if (waitForUpdate) {
    var delay = parseInt(waitForUpdate, 10);
    // As long as the delay is something sane (5 minutes or more), update
    // our delay time for requesting updates
    if (delay >= (5 * 60) && this.updateChecker_)
      this.updateChecker_.setDelay(delay * 1000);
  }

  // Let the backoff object know that we completed successfully.
  this.requestBackoff_.noteServerResponse(200);
}

/**
 * Callback function if the update request succeeded.
 * @param result String The error code of the failure
 */
PROT_ListManager.prototype.updateError_ = function(result) {
  G_Debug(this, "update error: " + result);
  // XXX: there was some trouble applying the updates.
}

/**
 * Callback function when the download failed
 * @param status String http status or an empty string if connection refused.
 */
PROT_ListManager.prototype.downloadError_ = function(status) {
  G_Debug(this, "download error: " + status);
  // 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.requestBackoff_.noteServerResponse(status);

  if (this.requestBackoff_.isErrorStatus(status)) {
    // Schedule an update for when our backoff is complete
    this.currentUpdateChecker_ =
      new G_Alarm(BindToObject(this.checkForUpdates, this),
                  this.requestBackoff_.nextRequestDelay());
  }
}

/**
 * Called when either the update process or a gethash request signals
 * that the server requested a rekey.
 */
PROT_ListManager.prototype.rekey_ = function() {
  G_Debug(this, "rekey requested");

  // The current key is no good anymore.
  this.keyManager_.dropKey();
  this.keyManager_.maybeReKey();
}

/**
 * Called when cookies are cleared - clears the current MAC keys.
 */
PROT_ListManager.prototype.cookieChanged_ = function(subject, topic, data) {
  if (data != "cleared")
    return;

  G_Debug(this, "cookies cleared");
  this.keyManager_.dropKey();
}

/**
 * Called when we've received a new key from the server.
 */
PROT_ListManager.prototype.newKey_ = function() {
  G_Debug(this, "got a new MAC key");

  this.hashCompleter_.setKeys(this.keyManager_.getClientKey(),
                              this.keyManager_.getWrappedKey());

  if (this.keyManager_.hasKey()) {
    if (this.updateWaitingForKey_) {
      this.updateWaitingForKey_ = false;
      this.checkForUpdates();
    }
  }
}

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;
}
//@line 42 "M:/central/mozilla/toolkit/components/url-classifier/src/nsUrlClassifierListManager.js"

var modScope = this;
function Init() {
  // Pull the library in.
  var jslib = Cc["@mozilla.org/url-classifier/jslib;1"]
              .getService().wrappedJSObject;
  Function.prototype.inherits = jslib.Function.prototype.inherits;
  modScope.G_Preferences = jslib.G_Preferences;
  modScope.G_PreferenceObserver = jslib.G_PreferenceObserver;
  modScope.G_ObserverServiceObserver = jslib.G_ObserverServiceObserver;
  modScope.G_Debug = jslib.G_Debug;
  modScope.G_Assert = jslib.G_Assert;
  modScope.G_debugService = jslib.G_debugService;
  modScope.G_Alarm = jslib.G_Alarm;
  modScope.BindToObject = jslib.BindToObject;
  modScope.PROT_XMLFetcher = jslib.PROT_XMLFetcher;
  modScope.PROT_UrlCryptoKeyManager = jslib.PROT_UrlCryptoKeyManager;
  modScope.RequestBackoff = jslib.RequestBackoff;

  // We only need to call Init once.
  modScope.Init = function() {};
}

// Module object
function UrlClassifierListManagerMod() {
  this.firstTime = true;
  this.cid = Components.ID("{ca168834-cc00-48f9-b83c-fd018e58cae3}");
  this.progid = "@mozilla.org/url-classifier/listmanager;1";
}

UrlClassifierListManagerMod.prototype.registerSelf = function(compMgr, fileSpec, loc, type) {
  if (this.firstTime) {
    this.firstTime = false;
    throw Components.results.NS_ERROR_FACTORY_REGISTER_AGAIN;
  }
  compMgr = compMgr.QueryInterface(Ci.nsIComponentRegistrar);
  compMgr.registerFactoryLocation(this.cid,
                                  "UrlClassifier List Manager Module",
                                  this.progid,
                                  fileSpec,
                                  loc,
                                  type);
};

UrlClassifierListManagerMod.prototype.getClassObject = function(compMgr, cid, iid) {  
  if (!cid.equals(this.cid))
    throw Components.results.NS_ERROR_NO_INTERFACE;
  if (!iid.equals(Ci.nsIFactory))
    throw Components.results.NS_ERROR_NOT_IMPLEMENTED;

  return this.factory;
}

UrlClassifierListManagerMod.prototype.canUnload = function(compMgr) {
  return true;
}

UrlClassifierListManagerMod.prototype.factory = {
  createInstance: function(outer, iid) {
    if (outer != null)
      throw Components.results.NS_ERROR_NO_AGGREGATION;
    Init();
    return (new PROT_ListManager()).QueryInterface(iid);
  }
};

var ListManagerModInst = new UrlClassifierListManagerMod();

function NSGetModule(compMgr, fileSpec) {
  return ListManagerModInst;
}
