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

// This is copied from toolkit/components/content/js/lang.js.
// It seems cleaner to copy this rather than #include from so far away.
Function.prototype.inherits = function(parentCtor) {
  var tempCtor = function(){};
  tempCtor.prototype = parentCtor.prototype;
  this.superClass_ = parentCtor.prototype;
  this.prototype = new tempCtor();
}  

/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Google Safe Browsing.
 *
 * The Initial Developer of the Original Code is Google Inc.
 * Portions created by the Initial Developer are Copyright (C) 2006
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Fritz Schneider <fritz@google.com> (original author)
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */


// This file implements an event registrar, an object with which you
// can register handlers for arbitrary programmer-defined
// events. Events are arbitrary strings and listeners are functions
// taking an object (stuffed with arguments) as a parameter. When you
// fire an event through the registrar, all listeners are invoked in
// an unspecified order. The firing function takes an object to be
// passed into each handler (it is _not_ copied, so be careful). We
// chose this calling convention so we don't have to change handler
// signatures when adding new information.
//
// Why not just use notifier/observers? Because passing data around
// with them requires either serialization or a new xpcom interface,
// both of which are a pain in the ass.
//
// Example:
//
// // Set up a listener
// this.handleTabload = function(e) {
//   foo(e.url);
//   bar(e.browser);
// };
//
// // Set up the registrar
// var eventTypes = ["tabload", "tabunload", "tabswitch"];
// var registrar = new EventRegistrar(eventTypes);
// var handler = BindToObject(this.handleTabload, this);
//
// // Register a listener
// registrar.registerListener("tabload", handler);
//
// // Fire an event and remove the listener
// var event = { "url": "http://www", "browser": browser };
// registrar.fire("tabload", event);
// registrar.removeListener("tabload", handler);
//
// TODO: could add ability to cancel further handlers by having listeners
//       return a boolean

/**
 * EventRegistrars are used to manage user-defined events. 
 *
 * @constructor
 * @param eventTypes {Array or Object} Array holding names of events or
 *                   Object holding properties the values of which are 
 *                   names (strings) for which listeners can register
 */
function EventRegistrar(eventTypes) {
  this.eventTypes = [];
  this.listeners_ = {};          // Listener sets, index by event type

  if (eventTypes instanceof Array) {
    var events = eventTypes;
  } else if (typeof eventTypes == "object") {
    var events = [];
    for (var e in eventTypes)
      events.push(eventTypes[e]);
  } else {
    throw new Error("Unrecognized init parameter to EventRegistrar");
  }

  for (var i = 0; i < events.length; i++) {
    this.eventTypes.push(events[i]);          // Copy in case caller mutates
    this.listeners_[events[i]] = 
      new ListDictionary(events[i] + "Listeners");
  }
}

/**
 * Indicates whether the given event is one the registrar can handle.
 * 
 * @param eventType {String} The name of the event to look up
 * @returns {Boolean} false if the event type is not known or the 
 *          event type string itself if it is
 */
EventRegistrar.prototype.isKnownEventType = function(eventType) {
  for (var i=0; i < this.eventTypes.length; i++)
    if (eventType == this.eventTypes[i])
      return eventType;
  return false;
}

/**
 * Add an event type to listen for.
 * @param eventType {String} The name of the event to add
 */
EventRegistrar.prototype.addEventType = function(eventType) {
  if (this.isKnownEventType(eventType))
    throw new Error("Event type already known: " + eventType);

  this.eventTypes.push(eventType);
  this.listeners_[eventType] = new ListDictionary(eventType + "Listeners");
}

/**
 * Register to receive events of the type passed in. 
 * 
 * @param eventType {String} indicating the event type (one of this.eventTypes)
 * @param listener {Function} to invoke when the event occurs.
 */
EventRegistrar.prototype.registerListener = function(eventType, listener) {
  if (this.isKnownEventType(eventType) === false)
    throw new Error("Unknown event type: " + eventType);

  this.listeners_[eventType].addMember(listener);
}

/**
 * Unregister a listener.
 * 
 * @param eventType {String} One of EventRegistrar.eventTypes' members
 * @param listener {Function} Function to remove as listener
 */
EventRegistrar.prototype.removeListener = function(eventType, listener) {
  if (this.isKnownEventType(eventType) === false)
    throw new Error("Unknown event type: " + eventType);

  this.listeners_[eventType].removeMember(listener);
}

/**
 * Invoke the handlers for the given eventType. 
 *
 * @param eventType {String} The event to fire
 * @param e {Object} Object containing the parameters of the event
 */
EventRegistrar.prototype.fire = function(eventType, e) {
  if (this.isKnownEventType(eventType) === false)
    throw new Error("Unknown event type: " + eventType);

  var invoke = function(listener) {
    listener(e);
  };

  this.listeners_[eventType].forEach(invoke);
}
/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Google Safe Browsing.
 *
 * The Initial Developer of the Original Code is Google Inc.
 * Portions created by the Initial Developer are Copyright (C) 2006
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Fritz Schneider <fritz@google.com> (original author)
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */


// This file implements a Dictionary data structure using a list
// (array). We could instead use an object, but using a list enables
// us to have ordering guarantees for iterators. The interface it exposes 
// is:
// 
// addMember(item)
// removeMember(item)
// isMember(item)
// forEach(func)
//
// TODO: this class isn't really a Dictionary, it's more like a 
//       membership set (i.e., a set without union and whatnot). We
//       should probably change the name to avoid confusion.

/**
 * Create a new Dictionary data structure.
 *
 * @constructor
 * @param name A string used to name the dictionary
 */
function ListDictionary(name) {
  this.name_ = name;
  this.members_ = [];
}

/**
 * Look an item up.
 *
 * @param item An item to look up in the dictionary
 * @returns Boolean indicating if the parameter is a member of the dictionary
 */
ListDictionary.prototype.isMember = function(item) {
  for (var i=0; i < this.members_.length; i++)
    if (this.members_[i] == item)
      return true;
  return false;
}

/**
 * Add an item
 *
 * @param item An item to add (does not check for dups)
 */
ListDictionary.prototype.addMember = function(item) {
  this.members_.push(item);
}

/**
 * Remove an item
 *
 * @param item The item to remove (doesn't check for dups)
 * @returns Boolean indicating if the item was removed
 */
ListDictionary.prototype.removeMember = function(item) {
  for (var i=0; i < this.members_.length; i++) {
    if (this.members_[i] == item) {
      for (var j=i; j < this.members_.length; j++)
        this.members_[j] = this.members_[j+1];

      this.members_.length--;
      return true;
    }
  }
  return false;
}

/**
 * Apply a function to each of the members. Does NOT replace the members
 * in the dictionary with results -- it just calls the function on each one.
 *
 * @param func Function to apply to the dictionary's members
 */
ListDictionary.prototype.forEach = function(func) {
  if (typeof func != "function")
    throw new Error("argument to forEach is not a function, it's a(n) " + 
                    typeof func);

  for (var i=0; i < this.members_.length; i++)
    func(this.members_[i]);
}
/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Google Safe Browsing.
 *
 * The Initial Developer of the Original Code is Google Inc.
 * Portions created by the Initial Developer are Copyright (C) 2006
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Fritz Schneider <fritz@google.com> (original author)
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */


// This file implements a G_TabbedBrowserWatcher, an object
// encapsulating and abstracting the mechanics of working with tabs
// and the documents within them. The watcher provides notification of
// various DOM-related events (a document loaded, a document unloaded,
// tab was created/destroyed, user switched tabs, etc.) as well as
// commonly required methods (get me the current tab, find the tab to
// which this document belongs, etc.).
//
// This class does not do progresslistener-based notifications; for that,
// use the NavWatcher.
//
// Note: I use "browser" and "tab" interchangeably.
//
// This class adds a level of indirection to event registration. You
// initialize it with a tabbedbrowser, and then register to hear
// events from it instead of from the tabbedbrowser or browser itself.
// Your handlers are invoked with a custom object as an argument (see
// below). This object contains useful information such as a reference
// to the browser in which the event is happening and whether the
// event is occurring on the top-level document in that browser.
//
// The events you can register to hear are:
//
// EVENT             DESCRIPTION
// -----             -----------
// load              Fires when a page is shown in the browser window and
//                   this page wasn't in the bfcache
//
// unload            Fires when a page is nav'd away from in the browser window
//                   and the page isn't going into the bfcache
//
// pageshow          Fires when a page is shown in the browser window, whether
//                   it came from bfcache or not. (There is a "persisted"
//                   property we can get from the event object if we'd like.
//                   It indicates whether the page was loaded from bfcache.
//                   If false then we known load recently fired).
//
// pagehide          Fires when a page is nav'd away from in the browser,
//                   whether its going into the bfcache or not. (There is
//                   a persisted property here as well that we're not
//                   propagating -- when its true the page is going into
//                   the bfcache, else it's not, and unload will shortly
//                   fire).
//
// domcontentloaded  BROKEN BROKEN BROKEN BROKEN BROKEN BROKEN BROKEN BROKEN
//                   Fires when a doc's DOM is ready, but its externally linked
//                   content hasn't necessarily loaded. This event is
//                   currently broken: it doesn't fire when using the
//                   forward/back buttons in conjunction with the bfcache.
//                   Bryner is working on a fix.
//
// tabload           Fires when a new tab has been created (but doesn't
//                   necessarily have content loaded in it)
//
// tabunload         Fires when a tab is being destroyed (and might have had
//                   the content it contains destroyed)
//
// tabswitch         Fires when the user switches tabs
//
// tabmove           Fires when a user drags a tab to a different position
//
//
// For pageshow, pagehide, load, unload, and domcontentloaded, the event
// object you'll receive has the following properties:
//
//      doc -- reference to Document on which the event fired
//      browser -- the browser in which the document resides
//      isTop -- boolean indicating if it is the top-level document
//      inSelected -- boolean indicating if it is in the currently selected tab
//
// For tabload and unload it has:
//
//      browser -- reference to the browser that is loading or closing
//
// For tabswitch it has:
//
//      fromBrowser -- reference to the browser user is switching from
//      toBrowser -- reference to the browser user is switching to
//
// For tabmove it has:
//
//      tab -- the tab that was moved (Note that this is the actual
//             tab widget that holds the document title, not the
//             browser object.  We use this because it's the target
//             of the DOMNodeInserted event.)
//      fromIndex -- the tab index before the move
//      toIndex -- the tab index after the move
//
//
// The order of events is:                   tabload
//                                           domcontentloaded
//                                           load
//                                           pageshow
//                                           --  --
//                                           pagehide
//                                           unload
//                                           tabunload
//
// Example:
//
// function handler(e /*event object*/) {
//   foo(e.browser);
// };
// var watcher = new G_TabbedBrowserWatcher(document.getElementById(gBrowser));
// watcher.registerListener("load", handler);      // register for events
// watcher.removeListener("load", handler);        // unregister
//
//
// TODO, BUGS, ISSUES, COMPLICATIONS:
//
// The only major problem is in closing tabs:
//
//     + When you close a tab, the reference to the Document you get in the
//       unload event is undefined. We pass this along. This could easily
//       be fixed by not listening for unload at all, but instead inferring
//       it from the information in pagehide, and then firing our own "fake"
//       unload after firing pagehide.
//
//     + There's no docshell during the pagehide event, so we can't determine
//       if the document is top-level. We pass undefined in this case.
//
//     + Though the browser reference during tabunload will be valid, its
//       members most likely have already been torn down. Use it in an
//       objectsafemap to keep state if you need its members.
//
//     + The event listener DOMNodeInserted has the potential to cause
//       performance problems if there are too many events fired.  It
//       should be ok, since we inserted it as far as possible into
//       the xul tree.
//
//
// TODO: need better enforcement of unique names. Two tabbedbrowserwatchers
//       with the same name will clobber each other because they use that
//       name to mark browsers they've seen.
// 
// TODO: the functions that iterate of windows and documents badly need
//       to be made cleaner. Right now we have multiple implementations
//       that essentially do the same thing :(
//
// But good enough for government work.

/**
 * Encapsulates tab-related information. You can use the
 * G_TabbedBrowserWatcher to watch for events on tabs as well as to
 * retrieve tab-related data (such as what tab is currently showing).
 * It receives many event notifications from G_BrowserWatchers it
 * attaches to newly opening tabs.
 *
 * @param tabBrowser A reference to the tabbed browser you wish to watch.
 *
 * @param name String containing a probabilistically unique name. Used to
 *             ensure that each tabbedbrowserwatcher can uniquely mark
 *             browser it has "seen."
 *
 * @param opt_filterAboutBlank Boolean indicating whether to filter events
 *                             for about:blank. These events are often
 *                             spurious since about:blank is the default
 *                             page for an empty browser.
 *
 * @constructor
 */

function G_TabbedBrowserWatcher(tabBrowser, name, opt_filterAboutBlank) {
  this.debugZone = "tabbedbrowserwatcher";
  this.registrar_ = new EventRegistrar(G_TabbedBrowserWatcher.events);
  this.tabBrowser_ = tabBrowser;
  this.filterAboutBlank_ = !!opt_filterAboutBlank;
  this.events = G_TabbedBrowserWatcher.events;      // Convenience pointer

  // We need some way to tell if we've seen a browser before, so we
  // set a property on it with a probabilistically unique string. The
  // string is a combination of a static string and one passed in by
  // the caller.
  G_Assert(this, typeof name == "string" && !!name,
           "Need a probabilistically unique name");
  this.name_ = name;
  this.mark_ = G_TabbedBrowserWatcher.mark_ + "-" + this.name_;

  this.tabbox_ = this.getTabBrowser().mTabBox;

  // There's no tabswitch event in Firefox, so we fake it by watching
  // for selects on the tabbox.
  this.onTabSwitchClosure_ = BindToObject(this.onTabSwitch, this);
  this.tabbox_.addEventListener("select",
                                this.onTabSwitchClosure_, true);

  // Used to determine when the user has switched tabs
  this.lastTab_ = this.getCurrentBrowser();
}

// Events for which listeners can register
G_TabbedBrowserWatcher.events = {
   TABSWITCH: "tabswitch",
   };

// We mark new tabs as we see them
G_TabbedBrowserWatcher.mark_ = "watcher-marked";

/**
 * Remove all the event handlers and clean up circular refs.
 */
G_TabbedBrowserWatcher.prototype.shutdown = function() {
  G_Debug(this, "Removing event listeners");
  if (this.tabbox_) {
    this.tabbox_.removeEventListener("select",
                                     this.onTabSwitchClosure_, true);
    // Break circular ref so we can be gc'ed.
    this.tabbox_ = null;
  }
  // Break circular ref so we can be gc'ed.
  if (this.lastTab_) {
    this.lastTab_ = null;
  }

  if (this.tabBrowser_) {
    this.tabBrowser_ = null;
  }
}

/**
 * Check to see if we've seen a browser before
 *
 * @param browser Browser to check
 * @returns Boolean indicating if we've attached a BrowserWatcher to this
 *          browser
 */
G_TabbedBrowserWatcher.prototype.isInstrumented_ = function(browser) {
  return !!browser[this.mark_];
}

/**
 * Attaches a BrowserWatcher to a browser and marks it as seen
 *
 * @param browser Browser to which to attach a G_BrowserWatcher
 */
G_TabbedBrowserWatcher.prototype.instrumentBrowser_ = function(browser) {
  G_Assert(this, !this.isInstrumented_(browser),
           "Browser already instrumented!");

  // The browserwatcher will hook itself into the browser and its parent (us)
  new G_BrowserWatcher(this, browser);
  browser[this.mark_] = true;
}

/**
 * Register to receive events of a particular type
 *
 * @param eventType String indicating the event (see
 *                  G_TabbedBrowserWatcher.events)
 * @param listener Function to invoke when the event occurs. See top-
 *                 level comments for parameters.
 */
G_TabbedBrowserWatcher.prototype.registerListener = function(eventType,
                                                             listener) {
  this.registrar_.registerListener(eventType, listener);
}

/**
 * Unregister a listener.
 *
 * @param eventType String one of G_TabbedBrowserWatcher.events' members
 * @param listener Function to remove as listener
 */
G_TabbedBrowserWatcher.prototype.removeListener = function(eventType,
                                                           listener) {
  this.registrar_.removeListener(eventType, listener);
}

/**
 * Send an event to all listeners for that type.
 *
 * @param eventType String indicating the event to trigger
 * @param e Object to pass to each listener (NOT copied -- be careful)
 */
G_TabbedBrowserWatcher.prototype.fire = function(eventType, e) {
  this.registrar_.fire(eventType, e);
}

/**
 * Convenience function to send a document-related event. We use this
 * convenience function because the event constructing logic and
 * parameters are the same for all these events. (Document-related
 * events are load, unload, pagehide, pageshow, and domcontentloaded).
 *
 * @param eventType String indicating the type of event to fire (one of
 *                  the document-related events)
 *
 * @param doc Reference to the HTMLDocument the event is occuring to
 *
 * @param browser Reference to the browser in which the document is contained
 */
G_TabbedBrowserWatcher.prototype.fireDocEvent_ = function(eventType,
                                                          doc,
                                                          browser) {
  // If we've already shutdown, don't bother firing any events.
  if (!this.tabBrowser_) {
    G_Debug(this, "Firing event after shutdown: " + eventType);
    return;
  }

  try {
    // Could be that the browser's contentDocument has already been torn
    // down. If so, this throws, and we can't tell without keeping more
    // state whether doc was the top frame.
    var isTop = (doc == browser.contentDocument);
  } catch(e) {
    var isTop = undefined;
  }

  var inSelected = (browser == this.getCurrentBrowser());

  var location = doc ? doc.location.href : undefined;

  // Only send notifications for about:config's if we're supposed to
  if (!this.filterAboutBlank_ || location != "about:blank") {

    G_Debug(this, "firing " + eventType + " for " + location +
            (isTop ? " (isTop)" : "") + (inSelected ? " (inSelected)" : ""));
    this.fire(eventType, { "doc": doc,
                           "isTop": isTop,
                           "inSelected": inSelected,
                           "browser": browser});
  }
}

/**
 * Invoked when the user might have switched tabs
 *
 * @param e Event object
 */
G_TabbedBrowserWatcher.prototype.onTabSwitch = function(e) {
  // Filter spurious events
  // The event target is usually tabs but can be tabpanels when tabs were opened
  // programatically via tabbrowser.addTab().
  if (e.target == null || 
      (e.target.localName != "tabs" && e.target.localName != "tabpanels"))
    return;

  var fromBrowser = this.lastTab_;
  var toBrowser = this.getCurrentBrowser();

  if (fromBrowser != toBrowser) {
    this.lastTab_ = toBrowser;
    G_Debug(this, "firing tabswitch");
    this.fire(this.events.TABSWITCH, { "fromBrowser": fromBrowser,
                                       "toBrowser": toBrowser });
  }
}

// Utility functions

/**
 * Returns a reference to the tabbed browser this G_TabbedBrowserWatcher
 * was initialized with.
 */
G_TabbedBrowserWatcher.prototype.getTabBrowser = function() {
  return this.tabBrowser_;
}

/**
 * Returns a reference to the currently selected tab.
 */
G_TabbedBrowserWatcher.prototype.getCurrentBrowser = function() {
  return this.getTabBrowser().selectedBrowser;
}

/**
 * Returns a reference to the top window in the currently selected tab.
 */
G_TabbedBrowserWatcher.prototype.getCurrentWindow = function() {
  return this.getCurrentBrowser().contentWindow;
}

/**
 * Find the browser corresponding to a Document
 *
 * @param doc Document we want the browser for
 * @returns Reference to the browser in which the given document is found
 *          or null if not found
 */
G_TabbedBrowserWatcher.prototype.getBrowserFromDocument = function(doc) {
  // Could instead get the top window of the browser in which the doc
  // is found via doc.defaultView.top, but sometimes the document
  // isn't in a browser at all (it's being unloaded, for example), so
  // defaultView won't be valid.

  // Helper: return true if doc is a sub-document of win
  function docInWindow(doc, win) {
    if (win.document == doc)
      return true;

    if (win.frames)
      for (var i = 0; i < win.frames.length; i++)
        if (docInWindow(doc, win.frames[i]))
          return true;

    return false;
  }

  var browsers = this.getTabBrowser().browsers;
  for (var i = 0; i < browsers.length; i++)
    if (docInWindow(doc, browsers[i].contentWindow))
      return browsers[i];

  return null;
}

/**
 * Find the Document that has the given URL loaded. Returns on the
 * _first_ such document found, so be careful.
 *
 * TODO make doc/window searches more elegant, and don't use inner functions
 *
 * @param url String indicating the URL we're searching for
 * @param opt_browser Optional reference to a browser. If given, the
 *                    search will be confined to only this browser.
 * @returns Reference to the Document with that URL or null if not found
 */
G_TabbedBrowserWatcher.prototype.getDocumentFromURL = function(url,
                                                               opt_browser) {

  // Helper function: return the Document in win that has location of url
  function docWithURL(win, url) {
    if (win.document.location.href == url)
      return win.document;

    if (win.frames)
      for (var i = 0; i < win.frames.length; i++) {
        var rv = docWithURL(win.frames[i], url);
  	if (rv)
          return rv;
      }

    return null;
  }

  if (opt_browser)
    return docWithURL(opt_browser.contentWindow, url);

  var browsers = this.getTabBrowser().browsers;
  for (var i=0; i < browsers.length; i++) {
    var rv = docWithURL(browsers[i].contentWindow, url);
    if (rv)
      return rv;
  }

  return null;
}

/**
 * Find the all Documents that have the given URL loaded.
 *
 * TODO make doc/window searches more elegant, and don't use inner functions
 *
 * @param url String indicating the URL we're searching for
 *
 * @param opt_browser Optional reference to a browser. If given, the
 *                    search will be confined to only this browser.
 *
 * @returns Array of Documents with the given URL (zero length if none found)
 */
G_TabbedBrowserWatcher.prototype.getDocumentsFromURL = function(url,
                                                                opt_browser) {

  var docs = [];

  // Helper function: add Docs in win with the location of url
  function getDocsWithURL(win, url) {
    if (win.document.location.href == url)
      docs.push(win.document);

    if (win.frames)
      for (var i = 0; i < win.frames.length; i++)
        getDocsWithURL(win.frames[i], url);
  }

  if (opt_browser)
    return getDocsWithURL(opt_browser.contentWindow, url);

  var browsers = this.getTabBrowser().browsers;
  for (var i=0; i < browsers.length; i++)
    getDocsWithURL(browsers[i].contentWindow, url);

  return docs;
}


/**
 * Finds the browser in which a Window resides.
 *
 * @param sub Window to find
 * @returns Reference to the browser in which sub resides, else null
 *          if not found
 */
G_TabbedBrowserWatcher.prototype.getBrowserFromWindow = function(sub) {

  // Helpfer function: return true if sub is a sub-window of win
  function containsSubWindow(sub, win) {
    if (win == sub)
      return true;

    if (win.frames)
      for (var i=0; i < win.frames.length; i++)
        if (containsSubWindow(sub, win.frames[i]))
          return true;

    return false;
  }

  var browsers = this.getTabBrowser().browsers;
  for (var i=0; i < browsers.length; i++)
    if (containsSubWindow(sub, browsers[i].contentWindow))
      return browsers[i];

  return null;
}

/**
 * Finds the XUL <tab> tag corresponding to a given browser.
 *
 * @param tabBrowser Reference to the tabbed browser in which browser lives
 * @param browser Reference to the browser we wish to find the tab of
 * @returns Reference to the browser's tab element, or null
 * @static
 */
G_TabbedBrowserWatcher.getTabElementFromBrowser = function(tabBrowser,
                                                           browser) {

  for (var i=0; i < tabBrowser.browsers.length; i++)
    if (tabBrowser.browsers[i] == browser)
      return tabBrowser.mTabContainer.childNodes[i];

  return null;
}
//@line 16 "/usr/obj/ports/mozilla-firefox-2.0.0.12/mozilla/browser/components/safebrowsing/src/nsSafebrowsingApplication.js"

/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Google Safe Browsing.
 *
 * The Initial Developer of the Original Code is Google Inc.
 * Portions created by the Initial Developer are Copyright (C) 2006
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Fritz Schneider <fritz@google.com> (original author)
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

// We instantiate this variable when we create the application.
var gDataProvider = null;

// An instance of our application is a PROT_Application object. It
// basically just populates a few globals and instantiates wardens and
// the listmanager.

/**
 * An instance of our application. There should be exactly one of these.
 * 
 * Note: This object should instantiated only at profile-after-change
 * or later because the listmanager and the cryptokeymanager need to
 * read and write data files. Additionally, NSS isn't loaded until
 * some time around then (Moz bug #321024).
 *
 * @constructor
 */
function PROT_Application() {
  this.debugZone= "application";

//@line 88 "/usr/obj/ports/mozilla-firefox-2.0.0.12/mozilla/browser/components/safebrowsing/src/../content/application.js"
  
  // expose some classes
  this.G_TabbedBrowserWatcher = G_TabbedBrowserWatcher;
  this.PROT_Controller = PROT_Controller;
  this.PROT_PhishingWarden = PROT_PhishingWarden;

  // Load data provider pref values
  gDataProvider = new PROT_DataProvider();

  // expose the object
  this.wrappedJSObject = this;
}

/**
 * @return String the report phishing URL (localized).
 */
PROT_Application.prototype.getReportPhishingURL = function() {
  return gDataProvider.getReportPhishURL();
}
/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Google Safe Browsing.
 *
 * The Initial Developer of the Original Code is Google Inc.
 * Portions created by the Initial Developer are Copyright (C) 2006
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Fritz Schneider <fritz@google.com> (original author)
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

// There is one BrowserView per browser window, and each BrowserView
// is responsible for keeping track of problems (phishy documents)
// within that window. The BrowserView is also responsible for
// figuring out what to do about such problems, for example, whether
// the tab with a phishy page is currently showing and therefore if we
// should be showing a warning.
// 
// The BrowserView receives information from three places:
//
// - from the phishing warden. When the phishing warden notices a
//   problem, it queries all browser views to see which one (if any)
//   has the Document that is problematic. It then hands the problem
//   off to the appropriate BrowserView.
// 
// - from the controller. The controller responds to explicit user 
//   actions (tab switches, requests to hide the warning message, 
//   etc.) and let's the BrowserView know about any user action 
//   having to do with the problems it is tracking.
//
// - from the TabbedBrowserWatcher. When the BrowserView is keeping
//   track of a problematic document it listens for interesting
//   events affecting it, for example pagehide (at which point
//   we presumably hide the warning if we're showing it).
//
// The BrowserView associates at most one "problem" with each Document
// in the browser window. It keeps state about which Documents are 
// problematic by storing a "problem queue" on each browser (tab).
// At most one problematic document per browser (tab) is active
// at any time. That is, we show the warning for at most one phishy
// document at any one time. If another phishy doc loads in that tab,
// it goes onto the end of the queue to be activated only when the
// currently active document goes away.
//
// If we had multiple types of warnings (one for after the page had
// loaded, one for when the user clicked a link, etc) here's where
// we'd select the appropate one to use. As it stands, we only have
// one displayer (an "afterload" displayer). A displayer knows _how_
// to display a warning, whereas as the BrowserView knows _what_ and
// _when_.
//
// To keep things (relatively) easy to reason about and efficient (the
// phishwarden could be querying us inside a progresslistener
// notification, or the controller inside an event handler), we have
// the following rules:
//
// - at most one of a displayer's start() or stop() methods is called
//   in any iteration (if calling two is required, the second is run in 
//   the next event loop)
// - displayers should run their operations synchronously so we don't have
//   to look two places (here and in the displayer) to see what is happening 
//   when
// - displayer actions are run after cleaning up the browser view state
//   in case they have consequences
//
// TODO: this could use some redesign, but I don't have time.
// TODO: the queue needs to be abstracted, but we want another release fast,
//       so I'm not going to touch it for the time being
// TODO: IDN issues and canonical URLs?
// TODO: Perhaps we should blur the page before showing a warning in order
//       to prevent stray keystrokes?

/**
 * The BrowerView is responsible for keeping track of and figuring out
 * what to do with problems within a single browser window.
 * 
 * TODO 
 * Unify all browser-related state here. Currently it's split
 * between two objects, this object and the controller. We could have
 * this object be solely responsible for UI hide/show decisions, which
 * would probably make it easier to reason about what's going on.
 * 
 * TODO 
 * Investigate an alternative model. For example, we could factor out
 * the problem signaling stuff from the tab/UI logic into a
 * ProblemRegistry. Attach listeners to new docs/requests as they go
 * by and have these listeners periodically check in with a
 * ProblemRegistry to see if they're watching a problematic
 * doc/request. If so, then have them flag the browser view to be
 * aware of the problem.
 *
 * @constructor
 * @param tabWatcher Reference to the TabbedBrowserWatcher we'll use to query 
 *                   for information about active tabs/browsers.
 * @param doc Reference to the XUL Document (browser window) in which the 
 *            tabwatcher is watching
 */ 
function PROT_BrowserView(tabWatcher, doc) {
  this.debugZone = "browserview";
  this.tabWatcher_ = tabWatcher;
  this.doc_ = doc;
}

/**
 * See if we have any Documents with a given (problematic) URL that
 * haven't yet been marked as problems. Called as a subroutine by
 * tryToHandleProblemRequest().
 *
 * @param url String containing the URL to look for
 *
 * @returns Reference to an unhandled Document with the problem URL or null
 */
PROT_BrowserView.prototype.getFirstUnhandledDocWithURL_ = function(url) {
  var docs = this.tabWatcher_.getDocumentsFromURL(url);
  if (!docs.length)
    return null;

  for (var i = 0; i < docs.length; i++) {
    // We only care about top level documents (i.e., we don't care about
    // frames).
    if (docs[i].defaultView.top != docs[i].defaultView)
      continue;

    var browser = this.tabWatcher_.getBrowserFromDocument(docs[i]);
    G_Assert(this, !!browser, "Found doc but can't find browser???");
    var alreadyHandled = this.getProblem_(docs[i], browser);

    if (!alreadyHandled)
      return docs[i];
  }
  return null;
}

/**
 * Invoked by the warden to give us the opportunity to handle a
 * problem.  A problem is signaled once per request for a problem
 * Document and is handled at most once, so there's no issue with us
 * "losing" a problem due to multiple concurrently loading Documents
 * with the same URL.
 *
 * @param warden Reference to the warden signalling the problem. We'll
 *               need him to instantiate one of his warning displayers
 * 
 * @param request The nsIRequest that is problematic
 *
 * @returns Boolean indicating whether we handled problem
 */
PROT_BrowserView.prototype.tryToHandleProblemRequest = function(warden,
                                                                request) {

  var doc = this.getFirstUnhandledDocWithURL_(request.name);
  if (doc) {
    var browser = this.tabWatcher_.getBrowserFromDocument(doc);
    G_Assert(this, !!browser, "Couldn't get browser from problem doc???");
    G_Assert(this, !this.getProblem_(doc, browser),
             "Doc is supposedly unhandled, but has state?");
    
    this.isProblemDocument_(browser, doc, warden);
    return true;
  }
  return false;
}

/**
 * We're sure a particular Document is problematic, so let's instantiate
 * a dispalyer for it and add it to the problem queue for the browser.
 *
 * @param browser Reference to the browser in which the problem doc resides
 *
 * @param doc Reference to the problematic document
 * 
 * @param warden Reference to the warden signalling the problem.
 */
PROT_BrowserView.prototype.isProblemDocument_ = function(browser, 
                                                         doc, 
                                                         warden) {

  G_Debug(this, "Document is problem: " + doc.location.href);
 
  var url = doc.location.href;

  // We only have one type of displayer right now
  var displayer = new warden.displayers_["afterload"]("Phishing afterload",
                                                      browser,
                                                      this.doc_,
                                                      url);

  // We listen for the problematic document being navigated away from
  // so we can remove it from the problem queue

  var hideHandler = BindToObject(this.onNavAwayFromProblem_, 
                                 this, 
                                 doc, 
                                 browser);
  doc.defaultView.addEventListener("pagehide", hideHandler, true);

  // More info than we technically need, but it comes in handy for debugging
  var problem = {
    "browser_": browser,
    "doc_": doc,
    "displayer_": displayer,
    "url_": url,
    "hideHandler_": hideHandler,
  };
  var numInQueue = this.queueProblem_(browser, problem);

  // If the queue was empty, schedule us to take something out
  if (numInQueue == 1)
    new G_Alarm(BindToObject(this.unqueueNextProblem_, this, browser), 0);
}

/**
 * Invoked when a problematic document is navigated away from. 
 *
 * @param doc Reference to the problematic Document navigated away from

 * @param browser Reference to the browser in which the problem document
 *                unloaded
 */
PROT_BrowserView.prototype.onNavAwayFromProblem_ = function(doc, browser) {

  G_Debug(this, "User nav'd away from problem.");
  var problem = this.getProblem_(doc, browser);
  (new PROT_Reporter).report("phishnavaway", problem.url_);

  G_Assert(this, doc === problem.doc_, "State doc not equal to nav away doc?");
  G_Assert(this, browser === problem.browser_, 
           "State browser not equal to nav away browser?");
  
  this.problemResolved(browser, doc);
}

/**
 * @param browser Reference to a browser we'd like to know about
 * 
 * @returns Boolean indicating if the browser in question has 
 *          problematic content
 */
PROT_BrowserView.prototype.hasProblem = function(browser) {
  return this.hasNonemptyProblemQueue_(browser);
}

/**
 * @param browser Reference to a browser we'd like to know about
 *
 * @returns Boolean indicating if the browser in question has a
 *          problem (i.e., it has a non-empty problem queue)
 */
PROT_BrowserView.prototype.hasNonemptyProblemQueue_ = function(browser) {
  try {
    return !!browser.PROT_problemState__ && 
      !!browser.PROT_problemState__.length;
  } catch(e) {
    // We could be checking a browser that has just been closed, in
    // which case its properties will not be valid, causing the above
    // statement to throw an error. Since this case handled elsewhere,
    // just return false.
    return false;
  }
}

/**
 * Invoked to indicate that the problem for a particular problematic
 * document in a browser has been resolved (e.g., by being navigated
 * away from).
 *
 * @param browser Reference to the browser in which resolution is happening
 *
 * @param opt_doc Reference to the problematic doc whose problem was resolved
 *                (if absent, assumes the doc assocaited with the currently
 *                active displayer)
 */
PROT_BrowserView.prototype.problemResolved = function(browser, opt_doc) {
  var problem;
  var doc;
  if (!!opt_doc) {
    doc = opt_doc;
    problem = this.getProblem_(doc, browser);
  } else {
    problem = this.getCurrentProblem_(browser);
    doc = problem.doc_;
  }

  problem.displayer_.done();
  var wasHead = this.deleteProblemFromQueue_(doc, browser);

  // Peek at the next problem (if any) in the queue for this browser
  var queueNotEmpty = this.getCurrentProblem_(browser);

  if (wasHead && queueNotEmpty) {
    G_Debug(this, "More problems pending. Scheduling unqueue.");
    new G_Alarm(BindToObject(this.unqueueNextProblem_, this, browser), 0);
  }
}

/**
 * Peek at the top of the problem queue and if there's something there,
 * make it active. 
 *
 * @param browser Reference to the browser we should activate a problem
 *                displayer in if one is available
 */
PROT_BrowserView.prototype.unqueueNextProblem_ = function(browser) {
  var problem = this.getCurrentProblem_(browser);
  if (!problem) {
    G_Debug(this, "No problem in queue; doc nav'd away from? (shrug)");
    return;
  }

  // Two problem docs that load in rapid succession could both schedule 
  // themselves to be unqueued before this method is called. So ensure 
  // that the problem at the head of the queue is not, in fact, active.
  if (!problem.displayer_.isActive()) {
    problem.displayer_.start();

    // Due to a bfcache bug, if we show the warning during paint
    // suppression, the collapsing of the content pane affects the doc we're
    // naving from :( The symptom is a page with grey screen on navigation
    // to or from a phishing page (the contentDocument will have width
    // zero).
    //
    // Paint supression lasts at most 250ms from when the parser sees
    // the body, and the parser sees the body well before it has a
    // height. We err on the side of caution.
    //
    // Thanks to bryner for helping to track the bfcache bug down.
    // https://bugzilla.mozilla.org/show_bug.cgi?id=319646
    
    if (this.tabWatcher_.getCurrentBrowser() === browser)
      new G_Alarm(BindToObject(this.problemBrowserMaybeSelected, 
                               this, 
                               browser),
                  350 /*ms*/);
  }
}

/**
 * Helper function that adds a new problem to the queue of problems pending
 * on this browser.
 *
 * @param browser Browser to which we should add state
 *
 * @param problem Object (structure, really) encapsulating the problem
 *
 * @returns Number indicating the number of items in the queue (and from
 *          which you can infer whether the recently added item was
 *          placed at the head, and hence should be active.
 */
PROT_BrowserView.prototype.queueProblem_ = function(browser, problem) {
  G_Debug(this, "Adding problem state for " + problem.url_);

  if (this.hasNonemptyProblemQueue_(browser))
    G_Debug(this, "Already has problem state. Queueing this problem...");

  // First problem ever signaled on this browser? Make a new queue!
  if (browser.PROT_problemState__ == undefined)
    browser.PROT_problemState__ = [];

  browser.PROT_problemState__.push(problem);
  return browser.PROT_problemState__.length;
}

/**
 * Helper function that removes a problem from the queue and deactivates
 * it.
 *
 * @param doc Reference to the doc for which we should remove state
 *
 * @param browser Reference to the browser from which we should remove
 *                state
 *
 * @returns Boolean indicating if the remove problem was currently active
 *          (that is, if it was at the head of the queue)
 */
PROT_BrowserView.prototype.deleteProblemFromQueue_ = function(doc, browser) {
  G_Debug(this, "Deleting problem state for " + browser);
  G_Assert(this, !!this.hasNonemptyProblemQueue_(browser),
           "Browser has no problem state");

  var problem = this.getProblem_(doc, browser);
  G_Assert(this, !!problem, "Couldnt find state in removeproblemstate???");

  var wasHead = browser.PROT_problemState__[0] === problem;
  this.removeProblemFromQueue_(doc, browser);

  var hideHandler = problem.hideHandler_;
  G_Assert(this, !!hideHandler, "No hidehandler in state?");
  problem.doc_.defaultView.removeEventListener("pagehide",
                                               hideHandler,
                                               true);
  return wasHead;
}

/**
 * Helper function that removes a problem from the queue but does 
 * NOT deactivate it.
 *
 * @param doc Reference to the doc for which we should remove state
 *
 * @param browser Reference to the browser from which we should remove
 *                state
 *
 * @returns Boolean indicating if the remove problem was currently active
 *          (that is, if it was at the head of the queue)
 */
PROT_BrowserView.prototype.removeProblemFromQueue_ = function(doc, browser) {
  G_Debug(this, "Removing problem state for " + browser);
  G_Assert(this, !!this.hasNonemptyProblemQueue_(browser),
           "Browser has no problem state");

  var problem = null;
  // TODO Blech. Let's please have an abstraction here instead.
  for (var i = 0; i < browser.PROT_problemState__.length; i++)
    if (browser.PROT_problemState__[i].doc_ === doc) {
      problem = browser.PROT_problemState__.splice(i, 1)[0];
      break;
    }
  return problem;
}

/**
 * Retrieve (but do not remove) the problem state for a particular
 * problematic Document in this browser
 *
 * @param doc Reference to the problematic doc to get state for
 *
 * @param browser Reference to the browser from which to get state
 *
 * @returns Object encapsulating the state we stored, or null if none
 */
PROT_BrowserView.prototype.getProblem_ = function(doc, browser) {
  if (!this.hasNonemptyProblemQueue_(browser))
    return null;

  // TODO Blech. Let's please have an abstraction here instead.
  for (var i = 0; i < browser.PROT_problemState__.length; i++)
    if (browser.PROT_problemState__[i].doc_ === doc)
      return browser.PROT_problemState__[i];
  return null;
}

/**
 * Retrieve the problem state for the currently active problem Document 
 * in this browser
 *
 * @param browser Reference to the browser from which to get state
 *
 * @returns Object encapsulating the state we stored, or null if none
 */
PROT_BrowserView.prototype.getCurrentProblem_ = function(browser) {
  return browser.PROT_problemState__[0];
}

/**
 * Invoked by the controller when the user switches tabs away from a problem 
 * tab. 
 *
 * @param browser Reference to the tab that was switched from
 */
PROT_BrowserView.prototype.problemBrowserUnselected = function(browser) {
  var problem = this.getCurrentProblem_(browser);
  G_Assert(this, !!problem, "Couldn't get state from browser");
  problem.displayer_.browserUnselected();
}

/**
 * Checks to see if the problem browser is selected, and if so, 
 * tell it it to show its warning.
 *
 * @param browser Reference to the browser we wish to check
 */
PROT_BrowserView.prototype.problemBrowserMaybeSelected = function(browser) {
  var problem = this.getCurrentProblem_(browser);

  if (this.tabWatcher_.getCurrentBrowser() === browser &&
      problem &&
      problem.displayer_.isActive()) 
    this.problemBrowserSelected(browser);
}

/**
 * Invoked by the controller when the user switches tabs to a problem tab
 *
 * @param browser Reference to the tab that was switched to
 */
PROT_BrowserView.prototype.problemBrowserSelected = function(browser) {
  G_Debug(this, "Problem browser selected");
  var problem = this.getCurrentProblem_(browser);
  G_Assert(this, !!problem, "No state? But we're selected!");
  problem.displayer_.browserSelected();
}

/**
 * Invoked by the controller when the user accepts our warning. Passes
 * the accept through to the message displayer, which knows what to do
 * (it will be displayer-specific).
 *
 * @param browser Reference to the browser for which the user accepted
 *                our warning
 */
PROT_BrowserView.prototype.acceptAction = function(browser) {
  var problem = this.getCurrentProblem_(browser);

  // We run the action only after we're completely through processing
  // this event. We do this because the action could cause state to be
  // cleared (e.g., by navigating the problem document) that we need
  // to finish processing the event.

  new G_Alarm(BindToObject(problem.displayer_.acceptAction, 
                           problem.displayer_), 
              0);
}

/**
 * Invoked by the controller when the user declines our
 * warning. Passes the decline through to the message displayer, which
 * knows what to do (it will be displayer-specific).
 *
 * @param browser Reference to the browser for which the user declined
 *                our warning
 */
PROT_BrowserView.prototype.declineAction = function(browser) {
  var problem = this.getCurrentProblem_(browser);
  G_Assert(this, !!problem, "User declined but no state???");

  // We run the action only after we're completely through processing
  // this event. We do this because the action could cause state to be
  // cleared (e.g., by navigating the problem document) that we need
  // to finish processing the event.

  new G_Alarm(BindToObject(problem.displayer_.declineAction, 
                           problem.displayer_), 
              0);
}

/**
 * The user wants to see the warning message. So let em! At some point when
 * we have multiple types of warnings, we'll have to mediate them here.
 *
 * @param browser Reference to the browser that has the warning the user 
 *                wants to see. 
 */
PROT_BrowserView.prototype.explicitShow = function(browser) {
  var problem = this.getCurrentProblem_(browser);
  G_Assert(this, !!problem, "Explicit show on browser w/o problem state???");
  problem.displayer_.explicitShow();
}
/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Google Safe Browsing.
 *
 * The Initial Developer of the Original Code is Google Inc.
 * Portions created by the Initial Developer are Copyright (C) 2006
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Fritz Schneider <fritz@google.com> (original author)
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */


// This is our controller -- the thingy that listens to what the user
// is doing. There is one controller per browser window, and each has
// a BrowserView that manages information about problems within the
// window. The controller figures out when the browser might want to
// know about something, but the browser view figures out what exactly
// to do (and the BrowserView's displayer figures out how to do it).
//
// For example, the controller might notice that the user has switched
// to a tab that has something problematic in it. It would tell its 
// BrowserView this, and the BrowserView would figure out whether it 
// is appropriate to show a warning (e.g., perhaps the user previously
// dismissed the warning for that problem). If so, the BrowserView tells
// the displayer to show the warning. Anyhoo...
//
// TODO Could move all browser-related hide/show logic into the browser
//      view. Need to think about this more.

/**
 * Handles user actions, translating them into messages to the view
 *
 * @constructor
 * @param win Reference to the Window (browser window context) we should
 *            attach to
 * @param tabWatcher  Reference to the TabbedBrowserWatcher object 
 *                    the controller should use to receive events about tabs.
 * @param phishingWarden Reference to the PhishingWarden we should register
 *                       our browserview with
 */
function PROT_Controller(win, tabWatcher, phishingWarden) {
  this.debugZone = "controller";

  this.win_ = win;
  this.phishingWarden_ = phishingWarden;

  // Use this to query preferences
  this.prefs_ = new G_Preferences();

  // Set us up to receive the events we want.
  this.tabWatcher_ = tabWatcher;
  this.onTabSwitchCallback_ = BindToObject(this.onTabSwitch, this);
  this.tabWatcher_.registerListener("tabswitch",
                                    this.onTabSwitchCallback_);


  // Install our command controllers. These commands are issued from
  // various places in our UI, including our preferences dialog, the
  // warning dialog, etc.
  var commandHandlers = { 
    "safebrowsing-show-warning" :
      BindToObject(this.onUserShowWarning, this),
    "safebrowsing-accept-warning" :
      BindToObject(this.onUserAcceptWarning, this),
    "safebrowsing-decline-warning" :
      BindToObject(this.onUserDeclineWarning, this),
  };

  this.commandController_ = new PROT_CommandController(commandHandlers);
  this.win_.controllers.appendController(this.commandController_);

  // This guy embodies the logic of when to display warnings
  // (displayers embody the how).
  this.browserView_ = new PROT_BrowserView(this.tabWatcher_, 
                                           this.win_.document);

  // We need to let the phishing warden know about this browser view so it 
  // can be given the opportunity to handle problem documents. We also need
  // to let the warden know when this window and hence this browser view
  // is going away.
  this.phishingWarden_.addBrowserView(this.browserView_);

  this.windowWatcher_ = 
    Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
    .getService(Components.interfaces.nsIWindowWatcher);

  G_Debug(this, "Controller initialized.");
}

/**
 * Invoked when the browser window is closing. Do some cleanup.
 */
PROT_Controller.prototype.shutdown = function(e) {
  G_Debug(this, "Browser window closing. Shutting controller down.");
  if (this.browserView_) {
    this.phishingWarden_.removeBrowserView(this.browserView_);
  }

  if (this.commandController_) {
    this.win_.controllers.removeController(this.commandController_);
    this.commandController_ = null;
  }


  // No need to drain the browser view's problem queue explicitly; it will
  // receive pagehides for all the browsers in its queues as they're torn
  // down, and it will remove them.
  this.browserView_ = null;

  if (this.tabWatcher_) {
    this.tabWatcher_.removeListener("tabswitch", 
                                    this.onTabSwitchCallback_);
    this.tabWatcher_.shutdown();
  }

  this.win_.removeEventListener("unload", this.onShutdown_, false);
  this.prefs_ = null;

  this.windowWatcher_ = null;

  G_Debug(this, "Controller shut down.");
}

/**
 * The user clicked the urlbar icon; they want to see the warning message
 * again.
 */
PROT_Controller.prototype.onUserShowWarning = function() {
  var browser = this.tabWatcher_.getCurrentBrowser();
  this.browserView_.explicitShow(browser);
}

/**
 * Deal with a user accepting our warning. 
 *
 * TODO the warning hide/display instructions here can probably be moved
 * into the browserview in the future, given its knowledge of when the
 * problem doc hides/shows.
 */
PROT_Controller.prototype.onUserAcceptWarning = function() {
  G_Debug(this, "User accepted warning.");
  var browser = this.tabWatcher_.getCurrentBrowser();
  G_Assert(this, !!browser, "Couldn't get current browser?!?");
  G_Assert(this, this.browserView_.hasProblem(browser),
           "User accept fired, but browser doesn't have warning showing?!?");

  this.browserView_.acceptAction(browser);
  this.browserView_.problemResolved(browser);
}

/**
 * Deal with a user declining our warning. 
 *
 * TODO the warning hide/display instructions here can probably be moved
 * into the browserview in the future, given its knowledge of when the
 * problem doc hides/shows.
 */
PROT_Controller.prototype.onUserDeclineWarning = function() {
  G_Debug(this, "User declined warning.");
  var browser = this.tabWatcher_.getCurrentBrowser();
  G_Assert(this, this.browserView_.hasProblem(browser),
           "User decline fired, but browser doesn't have warning showing?!?");
  this.browserView_.declineAction(browser);
  // We don't call problemResolved() here because all declining does it
  // hide the message; we still have the urlbar icon showing, giving
  // the user the ability to bring the warning message back up if they
  // so desire.
}

/**
 * Notice tab switches, and display or hide warnings as appropriate.
 *
 * TODO this logic can probably move into the browser view at some
 * point. But one thing at a time.
 */
PROT_Controller.prototype.onTabSwitch = function(e) {
  if (this.browserView_.hasProblem(e.fromBrowser)) 
    this.browserView_.problemBrowserUnselected(e.fromBrowser);

  if (this.browserView_.hasProblem(e.toBrowser))
    this.browserView_.problemBrowserSelected(e.toBrowser);
}
/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Google Safe Browsing.
 *
 * The Initial Developer of the Original Code is Google Inc.
 * Portions created by the Initial Developer are Copyright (C) 2006
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Fritz Schneider <fritz@google.com> (original author)
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */


// Some misc command-related plumbing used by the controller.


/**
 * A tiny wrapper class for super-simple command handlers.
 *
 * @param commandHandlerMap An object containing name/value pairs where
 *                          the name is command name (string) and value
 *                          is the function to execute for that command
 */
function PROT_CommandController(commandHandlerMap) {
  this.debugZone = "commandhandler";
  this.cmds_ = commandHandlerMap;
}

/**
 * @param cmd Command to query support for
 * @returns Boolean indicating if this controller supports cmd
 */
PROT_CommandController.prototype.supportsCommand = function(cmd) { 
  return (cmd in this.cmds_); 
}

/**
 * Trivial implementation
 *
 * @param cmd Command to query status of
 */
PROT_CommandController.prototype.isCommandEnabled = function(cmd) { 
  return true; 
}
  
/**
 * Execute a command
 *
 * @param cmd Command to execute
 */
PROT_CommandController.prototype.doCommand = function(cmd) {
  return this.cmds_[cmd](); 
}
 
/**
 * Trivial implementation
 */
PROT_CommandController.prototype.onEvent = function(cmd) { }

/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Google Safe Browsing.
 *
 * The Initial Developer of the Original Code is Google Inc.
 * Portions created by the Initial Developer are Copyright (C) 2006
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Fritz Schneider <fritz@google.com> (original author)
 *   J. Paul Reed <preed@mozilla.com>
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */


// A class that encapsulates data provider specific values.  The
// root of the provider pref tree is browser.safebrowsing.provider.
// followed by a number, followed by specific properties.  The properties
// that a data provider can supply are:
//
// name: The name of the provider
// lookupURL: The URL to send requests to in enhanced mode
// keyURL: Before we send URLs in enhanced mode, we need to encrypt them
// reportURL: When shown a warning bubble, we send back the user decision
//            (get me out of here/ignore warning) to this URL (strip cookies
//            first).  This is optional.
// reportGenericURL: HTML page for general user feedback
// reportPhishURL: HTML page for notifying the provider of a new phishing page
// reportErrorURL: HTML page for notifying the provider of a false positive

const kDataProviderIdPref = 'browser.safebrowsing.dataProvider';
const kProviderBasePref = 'browser.safebrowsing.provider.';

//@line 58 "/usr/obj/ports/mozilla-firefox-2.0.0.12/mozilla/browser/components/safebrowsing/src/../content/globalstore.js"
const MOZ_OFFICIAL_BUILD = true;
//@line 62 "/usr/obj/ports/mozilla-firefox-2.0.0.12/mozilla/browser/components/safebrowsing/src/../content/globalstore.js"

const MOZ_PARAM_LOCALE = /\{moz:locale\}/g;
const MOZ_PARAM_CLIENT = /\{moz:client\}/g;
const MOZ_PARAM_BUILDID = /\{moz:buildid\}/g;
const MOZ_PARAM_VERSION = /\{moz:version\}/g;

/**
 * Information regarding the data provider.
 */
function PROT_DataProvider() {
  this.prefs_ = new G_Preferences();

  this.loadDataProviderPrefs_();
  
  // Watch for changes in the data provider and update accordingly.
  this.prefs_.addObserver(kDataProviderIdPref,
                          BindToObject(this.loadDataProviderPrefs_, this));

  // Watch for when anti-phishing is toggled on or off.
  this.prefs_.addObserver(kPhishWardenEnabledPref,
                          BindToObject(this.loadDataProviderPrefs_, this));

  // Watch for when remote lookups are toggled on or off.
  this.prefs_.addObserver(kPhishWardenRemoteLookups,
                          BindToObject(this.loadDataProviderPrefs_, this));
}

/**
 * Populate all the provider variables.  We also call this when whenever
 * the provider id changes.
 */
PROT_DataProvider.prototype.loadDataProviderPrefs_ = function() {
  // Currently, there's no UI for changing local list provider so we
  // hard code the value for provider 0.
  this.updateURL_ = this.getUrlPref_(
        'browser.safebrowsing.provider.0.updateURL');

  var id = this.prefs_.getPref(kDataProviderIdPref, null);

  // default to 0
  if (null == id)
    id = 0;
  
  var basePref = kProviderBasePref + id + '.';

  this.name_ = this.prefs_.getPref(basePref + "name", "");

  // Urls used to get data from a provider
  this.lookupURL_ = this.getUrlPref_(basePref + "lookupURL");
  this.keyURL_ = this.getUrlPref_(basePref + "keyURL");
  this.reportURL_ = this.getUrlPref_(basePref + "reportURL");

  // Urls to HTML report pages
  this.reportGenericURL_ = this.getUrlPref_(basePref + "reportGenericURL");
  this.reportErrorURL_ = this.getUrlPref_(basePref + "reportErrorURL");
  this.reportPhishURL_ = this.getUrlPref_(basePref + "reportPhishURL");

  // Propogate the changes to the list-manager.
  this.updateListManager_();
}

/**
 * The list manager needs urls to operate.  It needs a url to know where the
 * table updates are, and it needs a url for decrypting enchash style tables.
 */
PROT_DataProvider.prototype.updateListManager_ = function() {
  var listManager = Cc["@mozilla.org/url-classifier/listmanager;1"]
                      .getService(Ci.nsIUrlListManager);

  // If we add support for changing local data providers, we need to add a
  // pref observer that sets the update url accordingly.
  listManager.setUpdateUrl(this.getUpdateURL());

  // setKeyUrl has the side effect of fetching a key from the server.
  // This shouldn't happen if anti-phishing is disabled or we're in local
  // list mode, so we need to check for that.
  var isEnabled = this.prefs_.getPref(kPhishWardenEnabledPref, false);
  var remoteLookups = this.prefs_.getPref(kPhishWardenRemoteLookups, false);
  if (isEnabled && remoteLookups) {
    listManager.setKeyUrl(this.getKeyURL());
  } else {
    // Clear the key to stop updates.
    listManager.setKeyUrl("");
  }
}

/**
 * Lookup the value of a URL from prefs file and do parameter substitution.
 */
PROT_DataProvider.prototype.getUrlPref_ = function(prefName) {
  var url = this.prefs_.getPref(prefName);

  var appInfo = Components.classes["@mozilla.org/xre/app-info;1"]
                          .getService(Components.interfaces.nsIXULAppInfo);

  var mozClientStr = MOZ_OFFICIAL_BUILD ? 'navclient-auto-ffox' : appInfo.name;

  // Parameter substitution
  url = url.replace(MOZ_PARAM_LOCALE, this.getLocale_());
  url = url.replace(MOZ_PARAM_CLIENT, mozClientStr);
  url = url.replace(MOZ_PARAM_BUILDID, appInfo.appBuildID);
  url = url.replace(MOZ_PARAM_VERSION, appInfo.version);
  return url;
}

/**
 * @return String the browser locale (similar code is in nsSearchService.js)
 */
PROT_DataProvider.prototype.getLocale_ = function() {
  const localePref = "general.useragent.locale";
  var locale = this.getLocalizedPref_(localePref);
  if (locale)
    return locale;

  // Not localized
  var prefs = new G_Preferences();
  return prefs.getPref(localePref, "");
}

/**
 * @return String name of the localized pref, null if none exists.
 */
PROT_DataProvider.prototype.getLocalizedPref_ = function(aPrefName) {
  // G_Preferences doesn't know about complex values, so we use the
  // xpcom object directly.
  var prefs = Cc["@mozilla.org/preferences-service;1"]
              .getService(Ci.nsIPrefBranch);
  try {
    return prefs.getComplexValue(aPrefName, Ci.nsIPrefLocalizedString).data;
  } catch (ex) {
  }
  return "";
}

//////////////////////////////////////////////////////////////////////////////
// Getters for the remote provider pref values mentioned above.
PROT_DataProvider.prototype.getName = function() {
  return this.name_;
}

PROT_DataProvider.prototype.getUpdateURL = function() {
  return this.updateURL_;
}

PROT_DataProvider.prototype.getLookupURL = function() {
  return this.lookupURL_;
}
PROT_DataProvider.prototype.getKeyURL = function() {
  return this.keyURL_;
}
PROT_DataProvider.prototype.getReportURL = function() {
  return this.reportURL_;
}

PROT_DataProvider.prototype.getReportGenericURL = function() {
  return this.reportGenericURL_;
}
PROT_DataProvider.prototype.getReportErrorURL = function() {
  return this.reportErrorURL_;
}
PROT_DataProvider.prototype.getReportPhishURL = function() {
  return this.reportPhishURL_;
}
/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Google Safe Browsing.
 *
 * The Initial Developer of the Original Code is Google Inc.
 * Portions created by the Initial Developer are Copyright (C) 2006
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Niels Provos <niels@google.com> (original author)d

 *   Fritz Schneider <fritz@google.com>
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

// A warden that knows how to register lists with a listmanager and keep them
// updated if necessary.  The ListWarden also provides a simple interface to
// check if a URL is evil or not.  Specialized wardens like the PhishingWarden
// inherit from it.
//
// Classes that inherit from ListWarden are responsible for calling
// enableTableUpdates or disableTableUpdates.  This usually entails
// registering prefObservers and calling enable or disable in the base
// class as appropriate.
//

/**
 * Abtracts the checking of user/browser actions for signs of
 * phishing. 
 *
 * @constructor
 */
function PROT_ListWarden() {
  this.debugZone = "listwarden";
  var listManager = Cc["@mozilla.org/url-classifier/listmanager;1"]
                      .getService(Ci.nsIUrlListManager);
  this.listManager_ = listManager;

  // Once we register tables, their respective names will be listed here.
  this.blackTables_ = [];
  this.whiteTables_ = [];
}

PROT_ListWarden.IN_BLACKLIST = 0
PROT_ListWarden.IN_WHITELIST = 1
PROT_ListWarden.NOT_FOUND = 2

/**
 * Tell the ListManger to keep all of our tables updated
 */

PROT_ListWarden.prototype.enableBlacklistTableUpdates = function() {
  for (var i = 0; i < this.blackTables_.length; ++i) {
    this.listManager_.enableUpdate(this.blackTables_[i]);
  }
}

/**
 * Tell the ListManager to stop updating our tables
 */

PROT_ListWarden.prototype.disableBlacklistTableUpdates = function() {
  for (var i = 0; i < this.blackTables_.length; ++i) {
    this.listManager_.disableUpdate(this.blackTables_[i]);
  }
}

/**
 * Tell the ListManager to update whitelist tables.  They may be enabled even
 * when other updates aren't, for performance reasons.
 */
PROT_ListWarden.prototype.enableWhitelistTableUpdates = function() {
  for (var i = 0; i < this.whiteTables_.length; ++i) {
    this.listManager_.enableUpdate(this.whiteTables_[i]);
  }
}

/**
 * Tell the ListManager to stop updating whitelist tables.
 */
PROT_ListWarden.prototype.disableWhitelistTableUpdates = function() {
  for (var i = 0; i < this.whiteTables_.length; ++i) {
    this.listManager_.disableUpdate(this.whiteTables_[i]);
  }
}

/**
 * Register a new black list table with the list manager
 * @param tableName - name of the table to register
 * @returns true if the table could be registered, false otherwise
 */

PROT_ListWarden.prototype.registerBlackTable = function(tableName) {
  var result = this.listManager_.registerTable(tableName, false);
  if (result) {
    this.blackTables_.push(tableName);
  }
  return result;
}

/**
 * Register a new white list table with the list manager
 * @param tableName - name of the table to register
 * @returns true if the table could be registered, false otherwise
 */

PROT_ListWarden.prototype.registerWhiteTable = function(tableName) {
  var result = this.listManager_.registerTable(tableName, false);
  if (result) {
    this.whiteTables_.push(tableName);
  }
  return result;
}

/**
 * Method that looks up a url on the whitelist.
 *
 * @param url The URL to check
 * @param callback Function with a single param:
 *       PROT_ListWarden.IN_BLACKLIST, PROT_ListWarden.IN_WHITELIST,
 *       or PROT_ListWarden.NOT_FOUND
 */
PROT_ListWarden.prototype.isWhiteURL = function(url, callback) {
  (new MultiTableQuerier(url,
                         this.whiteTables_,
                         [] /* no blacklists */,
                         callback)).run();
}

/**
 * Method that looks up a url in both the white and black lists.
 *
 * If there is conflict, the white list has precedence over the black list.
 *
 * This is tricky because all database queries are asynchronous.  So we need
 * to chain together our checks against white and black tables.  We use
 * MultiTableQuerier (see below) to manage this.
 *
 * @param url URL to look up
 * @param callback Function with a single param:
 *       PROT_ListWarden.IN_BLACKLIST, PROT_ListWarden.IN_WHITELIST,
 *       or PROT_ListWarden.NOT_FOUND
 */
PROT_ListWarden.prototype.isEvilURL = function(url, callback) {
  (new MultiTableQuerier(url,
                         this.whiteTables_,
                         this.blackTables_,
                         callback)).run();
}

/**
 * This class helps us query multiple tables even though each table check
 * is asynchronous.  It provides callbacks for each listManager lookup
 * and decides whether we need to continue querying or not.  After
 * instantiating the method, use run() to invoke.
 *
 * @param url String The url to check
 * @param whiteTables Array of strings with each white table name
 * @param blackTables Array of strings with each black table name
 * @param callback Function to call with result 
 *       PROT_ListWarden.IN_BLACKLIST, PROT_ListWarden.IN_WHITELIST,
 *       or PROT_ListWarden.NOT_FOUND
 */
function MultiTableQuerier(url, whiteTables, blackTables, callback) {
  this.debugZone = "multitablequerier";
  this.url_ = url;

  this.whiteTables_ = whiteTables;
  this.blackTables_ = blackTables;
  this.whiteIdx_ = 0;
  this.blackIdx_ = 0;

  this.callback_ = callback;
  this.listManager_ = Cc["@mozilla.org/url-classifier/listmanager;1"]
                      .getService(Ci.nsIUrlListManager);
}

/**
 * We first query the white tables in succession.  If any contain
 * the url, we stop.  If none contain the url, we query the black tables
 * in succession.  If any contain the url, we call callback and
 * stop.  If none of the black tables contain the url, then we just stop
 * (i.e., it's not black url).
 */
MultiTableQuerier.prototype.run = function() {
  var whiteTable = this.whiteTables_[this.whiteIdx_];
  var blackTable = this.blackTables_[this.blackIdx_];
  if (whiteTable) {
    //G_Debug(this, "Looking in whitetable: " + whiteTable);
    ++this.whiteIdx_;
    this.listManager_.safeExists(whiteTable, this.url_,
                                 BindToObject(this.whiteTableCallback_,
                                              this));
  } else if (blackTable) {
    //G_Debug(this, "Looking in blacktable: " + blackTable);
    ++this.blackIdx_;
    this.listManager_.safeExists(blackTable, this.url_,
                                 BindToObject(this.blackTableCallback_,
                                              this));
  } else {
    // No tables left to check, so we quit.
    G_Debug(this, "Not found in any tables: " + this.url_);
    this.callback_(PROT_ListWarden.NOT_FOUND);

    // Break circular ref to callback.
    this.callback_ = null;
    this.listManager_ = null;
  }
}

/**
 * After checking a white table, we return here.  If the url is found,
 * we can stop.  Otherwise, we call run again.
 */
MultiTableQuerier.prototype.whiteTableCallback_ = function(isFound) {
  //G_Debug(this, "whiteTableCallback_: " + isFound);
  if (!isFound)
    this.run();
  else {
    G_Debug(this, "Found in whitelist: " + this.url_)
    this.callback_(PROT_ListWarden.IN_WHITELIST);

    // Break circular ref to callback.
    this.callback_ = null;
    this.listManager_ = null;
  }
}

/**
 * After checking a black table, we return here.  If the url is found,
 * we can call the callback and stop.  Otherwise, we call run again.
 */
MultiTableQuerier.prototype.blackTableCallback_ = function(isFound) {
  //G_Debug(this, "blackTableCallback_: " + isFound);
  if (!isFound) {
    this.run();
  } else {
    // In the blacklist, must be an evil url.
    G_Debug(this, "Found in blacklist: " + this.url_)
    this.callback_(PROT_ListWarden.IN_BLACKLIST);

    // Break circular ref to callback.
    this.callback_ = null;
    this.listManager_ = null;
  }
}
/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Google Safe Browsing.
 *
 * The Initial Developer of the Original Code is Google Inc.
 * Portions created by the Initial Developer are Copyright (C) 2006
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Fritz Schneider <fritz@google.com> (original author)
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */


// Implementation of the warning message we show users when we
// notice navigation to a phishing page after it has loaded. The
// browser view encapsulates all the hide/show logic, so the displayer
// doesn't need to know when to display itself, only how.
//
// Displayers implement the following interface:
//
// start() -- fired to initialize the displayer (to make it active). When
//            called, this displayer starts listening for and responding to
//            events. At most one displayer per tab should be active at a 
//            time, and start() should be called at most once.
// declineAction() -- fired when the user declines the warning.
// acceptAction() -- fired when the user declines the warning
// explicitShow() -- fired when the user wants to see the warning again
// browserSelected() -- the browser is the top tab
// browserUnselected() -- the browser is no long the top tab
// done() -- clean up. May be called once (even if the displayer wasn't
//           activated).
//
// At the moment, all displayers share access to the same xul in 
// safebrowsing-overlay.xul. Hence the need for at most one displayer
// to be active per tab at a time.

/**
 * Factory that knows how to create a displayer appropriate to the
 * user's platform. We use a clunky canvas-based displayer for all
 * platforms until such time as we can overlay semi-transparent
 * areas over browser content.
 *
 * See the base object for a description of the constructor args
 *
 * @constructor
 */
function PROT_PhishMsgDisplayer(msgDesc, browser, doc, url) {

  // TODO: Change this to return a PhishMsgDisplayerTransp on windows
  // (and maybe other platforms) when Firefox 2.0 hits.

  return new PROT_PhishMsgDisplayerCanvas(msgDesc, browser, doc, url);
}


/**
 * Base class that implements most of the plumbing required to hide
 * and show a phishing warning. Subclasses implement the actual
 * showMessage and hideMessage methods. 
 *
 * This class is not meant to be instantiated directly.
 *
 * @param msgDesc String describing the kind of warning this is
 * @param browser Reference to the browser over which we display the msg
 * @param doc Reference to the document in which browser is found
 * @param url String containing url of the problem document
 * @constructor
 */
function PROT_PhishMsgDisplayerBase(msgDesc, browser, doc, url) {
  this.debugZone = "phishdisplayer";
  this.msgDesc_ = msgDesc;                                // currently unused
  this.browser_ = browser;
  this.doc_ = doc;
  this.url_ = url;

  // We'll need to manipulate the XUL in safebrowsing-overlay.xul
  this.messageId_ = "safebrowsing-palm-message";
  this.messageTailId_ = "safebrowsing-palm-message-tail-container";
  this.messageContentId_ = "safebrowsing-palm-message-content";
  this.extendedMessageId_ = "safebrowsing-palm-extended-message";
  this.showmoreLinkId_ = "safebrowsing-palm-showmore-link";
  this.faqLinkId_ = "safebrowsing-palm-faq-link";
  this.urlbarIconId_ = "safebrowsing-urlbar-icon";
  this.refElementId_ = this.urlbarIconId_;

  // We use this to report user actions to the server
  this.reporter_ = new PROT_Reporter();

  // The active UI elements in our warning send these commands; bind them
  // to their handlers but don't register the commands until we start
  // (because another displayer might be active)
  this.commandHandlers_ = {
    "safebrowsing-palm-showmore":
      BindToObject(this.showMore_, this),
  };

  this.windowWatcher_ = 
    Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
    .getService(Components.interfaces.nsIWindowWatcher);
}

/**
 * @returns The default background color of the browser
 */
PROT_PhishMsgDisplayerBase.prototype.getBackgroundColor_ = function() {
  var pref = Components.classes["@mozilla.org/preferences-service;1"].
             getService(Components.interfaces.nsIPrefBranch);
  return pref.getCharPref("browser.display.background_color");
}

/**
 * Fired when the user declines our warning. Report it!
 */
PROT_PhishMsgDisplayerBase.prototype.declineAction = function() {
  G_Debug(this, "User declined warning.");
  G_Assert(this, this.started_, "Decline on a non-active displayer?");
  this.reporter_.report("phishdecline", this.url_);

  this.messageShouldShow_ = false;
  if (this.messageShowing_)
    this.hideMessage_();
}

/**
 * Fired when the user accepts our warning
 */
PROT_PhishMsgDisplayerBase.prototype.acceptAction = function() {
  G_Assert(this, this.started_, "Accept on an unstarted displayer?");
  G_Assert(this, this.done_, "Accept on a finished displayer?");
  G_Debug(this, "User accepted warning.");
  this.reporter_.report("phishaccept", this.url_);

  var url = this.getMeOutOfHereUrl_();
  this.browser_.loadURI(url);
}

/**
 * Get the url for "Get me out of here."  This is the browser's default home
 * page, or, about:blank.
 * @return String url
 */
PROT_PhishMsgDisplayerBase.prototype.getMeOutOfHereUrl_ = function() {
  // Try to get their homepage from prefs.
  var prefs = Cc["@mozilla.org/preferences-service;1"]
              .getService(Ci.nsIPrefService).getDefaultBranch(null);

  var url = "about:blank";
  try {
    url = prefs.getComplexValue("browser.startup.homepage",
                                Ci.nsIPrefLocalizedString).data;
    // If url is a pipe-delimited set of pages, just take the first one.
    // This will need to change once bug 221445 is fixed.
    if (url.indexOf("|") != -1)
      url = url.split("|")[0];
  } catch(e) {
    G_Debug(this, "Couldn't get homepage pref: " + e);
  }
  
  return url;
}

/**
 * Invoked when the browser is resized
 */
PROT_PhishMsgDisplayerBase.prototype.onBrowserResized_ = function(event) {
  G_Debug(this, "Got resize for " + event.target);

  if (this.messageShowing_) {
    this.hideMessage_(); 
    this.showMessage_();
  }
}

/**
 * Invoked by the browser view when our browser is switched to
 */
PROT_PhishMsgDisplayerBase.prototype.browserSelected = function() {
  G_Assert(this, this.started_, "Displayer selected before being started???");

  // If messageshowing hasn't been set, then this is the first time this
  // problematic browser tab has been on top, so do our setup and show
  // the warning.
  if (this.messageShowing_ === undefined) {
    this.messageShouldShow_ = true;
  }

  this.hideLockIcon_();        // Comes back when we are unselected or unloaded
  this.addWarningInUrlbar_();  // Goes away when we are unselected or unloaded

  // messageShouldShow might be false if the user dismissed the warning, 
  // switched tabs, and then switched back. We're still active, but don't
  // want to show the warning again. The user can cause it to show by
  // clicking our icon in the urlbar.
  if (this.messageShouldShow_)
    this.showMessage_();
}

/**
 * Invoked to display the warning message explicitly, for example if the user
 * clicked the url warning icon.
 */
PROT_PhishMsgDisplayerBase.prototype.explicitShow = function() {
  this.messageShouldShow_ = true;
  if (!this.messageShowing_)
    this.showMessage_();
}

/** 
 * Invoked by the browser view when our browser is switched away from
 */
PROT_PhishMsgDisplayerBase.prototype.browserUnselected = function() {
  this.removeWarningInUrlbar_();
  this.unhideLockIcon_();
  if (this.messageShowing_)
    this.hideMessage_();
}

/**
 * Invoked to make this displayer active. The displayer will now start
 * responding to notifications such as commands and resize events. We
 * can't do this in the constructor because there might be many 
 * displayers instantiated waiting in the problem queue for a particular
 * browser (e.g., a page has multiple framed problem pages), and we
 * don't want them all responding to commands!
 *
 * Invoked zero (the page we're a warning for was nav'd away from
 * before it reaches the head of the problem queue) or one (we're
 * displaying this warning) times by the browser view.
 */
PROT_PhishMsgDisplayerBase.prototype.start = function() {
  G_Assert(this, this.started_ == undefined, "Displayer started twice?");
  this.started_ = true;

  this.commandController_ = new PROT_CommandController(this.commandHandlers_);
  this.doc_.defaultView.controllers.appendController(this.commandController_);

  // Add an event listener for when the browser resizes (e.g., user
  // shows/hides the sidebar).
  this.resizeHandler_ = BindToObject(this.onBrowserResized_, this);
  this.browser_.addEventListener("resize",
                                 this.resizeHandler_, 
                                 false);
}

/**
 * @returns Boolean indicating whether this displayer is currently
 *          active
 */
PROT_PhishMsgDisplayerBase.prototype.isActive = function() {
  return !!this.started_;
}

/**
 * Invoked by the browser view to clean up after the user is done 
 * interacting with the message. Should be called once by the browser
 * view. 
 */
PROT_PhishMsgDisplayerBase.prototype.done = function() {
  G_Assert(this, !this.done_, "Called done more than once?");
  this.done_ = true;

  // If the Document we're showing the warning for was nav'd away from
  // before we had a chance to get started, we have nothing to do.
  if (this.started_) {

    // If we were started, we must be the current problem, so these things
    // must be showing
    this.removeWarningInUrlbar_();
    this.unhideLockIcon_();

    // Could be though that they've closed the warning dialog
    if (this.messageShowing_)
      this.hideMessage_();

    if (this.resizeHandler_) {
      this.browser_.removeEventListener("resize", 
                                        this.resizeHandler_, 
                                        false);
      this.resizeHandler_ = null;
    }
    
    var win = this.doc_.defaultView;
    win.controllers.removeController(this.commandController_);
    this.commandController_ = null;
  }
}

/**
 * Helper function to remove a substring from inside a string.
 *
 * @param orig String to remove substring from
 * 
 * @param toRemove String to remove (if it is present)
 *
 * @returns String with the substring removed
 */
PROT_PhishMsgDisplayerBase.prototype.removeIfExists_ = function(orig,
                                                                toRemove) {
  var pos = orig.indexOf(toRemove);
  if (pos != -1)
    orig = orig.substring(0, pos) + orig.substring(pos + toRemove.length);

  return orig;
}

/**
 * We don't want to confuse users if they land on a phishy page that uses
 * SSL, so ensure that the lock icon never shows when we're showing our 
 * warning.
 */
PROT_PhishMsgDisplayerBase.prototype.hideLockIcon_ = function() {
  var lockIcon = this.doc_.getElementById("lock-icon");
  if (!lockIcon)
    return;
  lockIcon.hidden = true;
}

/**
 * Ensure they can see it after our warning is finished.
 */
PROT_PhishMsgDisplayerBase.prototype.unhideLockIcon_ = function() {
  var lockIcon = this.doc_.getElementById("lock-icon");
  if (!lockIcon)
    return;
  lockIcon.hidden = false;
}

/**
 * This method makes our warning icon visible in the location bar. It will
 * be removed only when the problematic document is navigated awy from 
 * (i.e., when done() is called), and not when the warning is dismissed.
 */
PROT_PhishMsgDisplayerBase.prototype.addWarningInUrlbar_ = function() {
  var urlbarIcon = this.doc_.getElementById(this.urlbarIconId_);
  if (!urlbarIcon)
    return;
  urlbarIcon.setAttribute('level', 'warn');
}

/**
 * Hides our urlbar icon
 */
PROT_PhishMsgDisplayerBase.prototype.removeWarningInUrlbar_ = function() {
  var urlbarIcon = this.doc_.getElementById(this.urlbarIconId_);
  if (!urlbarIcon)
    return;
  urlbarIcon.setAttribute('level', 'safe');
}

/**
 * VIRTUAL -- Displays the warning message
 */
PROT_PhishMsgDisplayerBase.prototype.showMessage_ = function() { };

/**
 * VIRTUAL -- Hide the warning message from the user.
 */
PROT_PhishMsgDisplayerBase.prototype.hideMessage_ = function() { };

/**
 * Reposition the message relative to refElement in the parent window
 *
 * @param message Reference to the element to position
 * @param tail Reference to the message tail
 * @param refElement Reference to element relative to which we position
 *                   ourselves
 */
PROT_PhishMsgDisplayerBase.prototype.adjustLocation_ = function(message,
                                                                tail,
                                                                refElement) {
  var refX = refElement.boxObject.x;
  var refY = refElement.boxObject.y;
  var refHeight = refElement.boxObject.height;
  var refWidth = refElement.boxObject.width;
  G_Debug(this, "Ref element is at [window-relative] (" + refX + ", " + 
          refY + ")");

  var pixelsIntoRefY = -2;
  var tailY = refY + refHeight - pixelsIntoRefY;
  var tailPixelsLeftOfRefX = tail.boxObject.width;
  var tailPixelsIntoRefX = Math.round(refWidth / 2);
  var tailX = refX - tailPixelsLeftOfRefX + tailPixelsIntoRefX;

  // Move message up a couple pixels so the tail overlaps it.
  var messageY = tailY + tail.boxObject.height - 2;
  var messagePixelsLeftOfRefX = 375;
  var messageX = refX - messagePixelsLeftOfRefX;
  G_Debug(this, "Message is at [window-relative] (" + messageX + ", " + 
          messageY + ")");
  G_Debug(this, "Tail is at [window-relative] (" + tailX + ", " + 
          tailY + ")");

  if (messageX < 0) {
    // We're hanging off the left edge, switch to floating mode
    tail.style.display = "none";
    this.adjustLocationFloating_(message);
    return;
  }

  tail.style.top = tailY + "px";
  tail.style.left = tailX + "px";
  message.style.top = messageY + "px";
  message.style.left = messageX + "px";
  
  this.maybeAddScrollbars_();
}

/**
 * Position the warning bubble with no reference element.  In this case we
 * just center the warning bubble at the top of the users window.
 * @param message XULElement message bubble XUL container.
 */
PROT_PhishMsgDisplayerBase.prototype.adjustLocationFloating_ = function(message) {
  // Compute X offset
  var browserX = this.browser_.boxObject.x;
  var browserXCenter = browserX + this.browser_.boxObject.width / 2;
  var messageX = browserXCenter - (message.boxObject.width / 2);

  // Compute Y offset (top of the browser window)
  var messageY = this.browser_.boxObject.y;

  // Position message
  message.style.top = messageY + "px";
  message.style.left = messageX + "px";

  this.maybeAddScrollbars_();
}

/**
 * Add a vertical scrollbar if we're falling out of the browser window.
 */
PROT_PhishMsgDisplayerBase.prototype.maybeAddScrollbars_ = function() {
  var message = this.doc_.getElementById(this.messageId_);
  
  var content = this.doc_.getElementById(this.messageContentId_);
  var bottom = content.boxObject.y + content.boxObject.height;
  var maxY = this.doc_.defaultView.innerHeight;
  G_Debug(this, "bottom: " + bottom + ", maxY: " + maxY
                + ", new height: " + (maxY - content.boxObject.y));
  if (bottom > maxY) {
    var newHeight = maxY - content.boxObject.y;
    if (newHeight < 1)
      newHeight = 1;

    content.style.height = newHeight + "px";
    content.style.overflow = "auto";
  }
}

/**
 * Show the extended warning message
 */
PROT_PhishMsgDisplayerBase.prototype.showMore_ = function() {
  this.doc_.getElementById(this.extendedMessageId_).hidden = false;
  this.doc_.getElementById(this.showmoreLinkId_).style.display = "none";

  // set FAQ URL
  var formatter = Components.classes["@mozilla.org/toolkit/URLFormatterService;1"]
                            .getService(Components.interfaces.nsIURLFormatter);
  var faqURL = formatter.formatURLPref("browser.safebrowsing.warning.infoURL");
  var labelEl = this.doc_.getElementById(this.faqLinkId_);
  labelEl.setAttribute("href", faqURL);
  
  this.maybeAddScrollbars_();
}

/**
 * The user clicked on one of the links in the buble.  Display the
 * corresponding page in a new window with all the chrome enabled.
 *
 * @param url The URL to display in a new window
 */
PROT_PhishMsgDisplayerBase.prototype.showURL_ = function(url) {
  this.windowWatcher_.openWindow(this.windowWatcher_.activeWindow,
                                 url,
                                 "_blank",
                                 null,
                                 null);
}

/**
 * If the warning bubble came up in error, this url goes to a form
 * to notify the data provider.
 * @return url String
 */
PROT_PhishMsgDisplayerBase.prototype.getReportErrorURL_ = function() {
  var badUrl = this.url_;

  var url = gDataProvider.getReportErrorURL();
  url += "&url=" + encodeURIComponent(badUrl);
  return url;
}

/**
 * URL for the user to report back to us.  This is to provide the user
 * with an action after being warned.
 */
PROT_PhishMsgDisplayerBase.prototype.getReportGenericURL_ = function() {
  var badUrl = this.url_;

  var url = gDataProvider.getReportGenericURL();
  url += "&url=" + encodeURIComponent(badUrl);
  return url;
}


/**
 * A specific implementation of the dislpayer using a canvas. This
 * class is meant for use on platforms that don't support transparent
 * elements over browser content (currently: all platforms). 
 *
 * The main ugliness is the fact that we're hiding the content area and
 * painting the page to canvas. As a result, we must periodically
 * re-paint the canvas to reflect updates to the page. Otherwise if
 * the page was half-loaded when we showed our warning, it would
 * stay that way even though the page actually finished loading. 
 *
 * See base constructor for full details of constructor args.
 *
 * @constructor
 */
function PROT_PhishMsgDisplayerCanvas(msgDesc, browser, doc, url) {
  PROT_PhishMsgDisplayerBase.call(this, msgDesc, browser, doc, url);

  this.dimAreaId_ = "safebrowsing-dim-area-canvas";
  this.pageCanvasId_ = "safebrowsing-page-canvas";
  this.xhtmlNS_ = "http://www.w3.org/1999/xhtml";     // we create html:canvas
}

PROT_PhishMsgDisplayerCanvas.inherits(PROT_PhishMsgDisplayerBase);

/**
 * Displays the warning message.  First we make sure the overlay is loaded
 * then call showMessageAfterOverlay_.
 */
PROT_PhishMsgDisplayerCanvas.prototype.showMessage_ = function() {
  G_Debug(this, "Showing message.");

  // Load the overlay if we haven't already.
  var dimmer = this.doc_.getElementById('safebrowsing-dim-area-canvas');
  if (!dimmer) {
    var onOverlayMerged = BindToObject(this.showMessageAfterOverlay_,
                                       this);
    var observer = new G_ObserverWrapper("xul-overlay-merged",
                                         onOverlayMerged);

    this.doc_.loadOverlay(
        "chrome://browser/content/safebrowsing/warning-overlay.xul",
        observer);
  } else {
    // The overlay is already loaded so we go ahead and call
    // showMessageAfterOverlay_.
    this.showMessageAfterOverlay_();
  }
}

/**
 * This does the actual work of showing the warning message.
 */
PROT_PhishMsgDisplayerCanvas.prototype.showMessageAfterOverlay_ = function() {
  this.messageShowing_ = true;

  // Position the canvas overlay. Order here is significant, but don't ask me
  // why for some of these. You need to:
  // 1. get browser dimensions
  // 2. add canvas to the document
  // 3. unhide the dimmer (gray out overlay)
  // 4. display to the canvas
  // 5. unhide the warning message
  // 6. update link targets in warning message
  // 7. focus the warning message

  // (1)
  var w = this.browser_.boxObject.width;
  G_Debug(this, "browser w=" + w);
  var h = this.browser_.boxObject.height;
  G_Debug(this, "browser h=" + h);
  var x = this.browser_.boxObject.x;
  G_Debug(this, "browser x=" + w);
  var y = this.browser_.boxObject.y;
  G_Debug(this, "browser y=" + h);

  var win = this.browser_.contentWindow;
  var scrollX = win.scrollX;
  G_Debug(this, "win scrollx=" + scrollX);
  var scrollY = win.scrollY;
  G_Debug(this, "win scrolly=" + scrollY);

  // (2)
  // We add the canvas dynamically and remove it when we're done because
  // leaving it hanging around consumes a lot of memory.
  var pageCanvas = this.doc_.createElementNS(this.xhtmlNS_, "html:canvas");
  pageCanvas.id = this.pageCanvasId_;
  pageCanvas.style.left = x + 'px';
  pageCanvas.style.top = y + 'px';

  var dimarea = this.doc_.getElementById(this.dimAreaId_);
  this.doc_.getElementById('main-window').insertBefore(pageCanvas,
                                                       dimarea);

  // (3)
  dimarea.style.left = x + 'px';
  dimarea.style.top = y + 'px';
  dimarea.style.width = w + 'px';
  dimarea.style.height = h + 'px';
  dimarea.hidden = false;
  
  // (4)
  pageCanvas.setAttribute("width", w);
  pageCanvas.setAttribute("height", h);

  var bgcolor = this.getBackgroundColor_();

  var cx = pageCanvas.getContext("2d");
  cx.drawWindow(win, scrollX, scrollY, w, h, bgcolor);

  // Now repaint the window every so often in case the content hasn't fully
  // loaded at this point.
  var debZone = this.debugZone;
  function repaint() {
    G_Debug(debZone, "Repainting canvas...");
    cx.drawWindow(win, scrollX, scrollY, w, h, bgcolor);
  };
  this.repainter_ = new PROT_PhishMsgCanvasRepainter(repaint);

  // (5)
  this.showAndPositionWarning_();

  // (6)
  var link = this.doc_.getElementById('safebrowsing-palm-falsepositive-link');
  link.href = this.getReportErrorURL_();

  // (7)
  this.doc_.getElementById(this.messageContentId_).focus();
}

/**
 * Show and position the warning message.  We position the waring message
 * relative to the icon in the url bar, but if the element doesn't exist,
 * (e.g., the user remove the url bar from her/his chrome), we anchor at the
 * top of the window.
 */
PROT_PhishMsgDisplayerCanvas.prototype.showAndPositionWarning_ = function() {
  var refElement = this.doc_.getElementById(this.refElementId_);
  var message = this.doc_.getElementById(this.messageId_);
  var tail = this.doc_.getElementById(this.messageTailId_);

  message.hidden = false;
  message.style.display = "block";

  // Determine if the refElement is visible.
  if (this.isVisibleElement_(refElement)) {
    // Show tail and position warning relative to refElement.
    tail.hidden = false;
    tail.style.display = "block";
    this.adjustLocation_(message, tail, refElement);
  } else {
    // No ref element, position in the top center of window.
    tail.hidden = true;
    tail.style.display = "none";
    this.adjustLocationFloating_(message);
  }
}

/**
 * @return Boolean true if elt is a visible XUL element.
 */
PROT_PhishMsgDisplayerCanvas.prototype.isVisibleElement_ = function(elt) {
  if (!elt)
    return false;
  
  // If it's on a collapsed/hidden toolbar, the x position is set to 0.
  if (elt.boxObject.x == 0)
    return false;

  return true;
}

/**
 * Hide the warning message from the user.
 */
PROT_PhishMsgDisplayerCanvas.prototype.hideMessage_ = function() {
  G_Debug(this, "Hiding phishing warning.");
  G_Assert(this, this.messageShowing_, "Hide message called but not showing?");

  this.messageShowing_ = false;
  this.repainter_.cancel();
  this.repainter_ = null;

  // Hide the warning popup.
  var message = this.doc_.getElementById(this.messageId_);
  message.hidden = true;
  message.style.display = "none";
  var content = this.doc_.getElementById(this.messageContentId_);
  content.style.height = "";
  content.style.overflow = "";

  var tail = this.doc_.getElementById(this.messageTailId_);
  tail.hidden = true;
  tail.style.display = "none";

  // Remove the canvas element from the chrome document.
  var pageCanvas = this.doc_.getElementById(this.pageCanvasId_);
  pageCanvas.parentNode.removeChild(pageCanvas);

  // Hide the dimmer.
  var dimarea = this.doc_.getElementById(this.dimAreaId_);
  dimarea.hidden = true;
}


/**
 * Helper class that periodically repaints the canvas. We repaint
 * frequently at first, and then back off to a less frequent schedule
 * at "steady state," and finally just stop altogether. We have to do
 * this because we're not sure if the page has finished loading when
 * we first paint the canvas, and because we want to reflect any
 * dynamically written content into the canvas as it appears in the
 * page after load.
 *
 * @param repaintFunc Function to call to repaint browser.
 *
 * @constructor
 */
function PROT_PhishMsgCanvasRepainter(repaintFunc) {
  this.count_ = 0;
  this.repaintFunc_ = repaintFunc;
  this.initPeriodMS_ = 500;             // Initially repaint every 500ms
  this.steadyStateAtMS_ = 10 * 1000;    // Go slowly after 10 seconds,
  this.steadyStatePeriodMS_ = 3 * 1000; // repainting every 3 seconds, and
  this.quitAtMS_ = 20 * 1000;           // stop after 20 seconds
  this.startMS_ = (new Date).getTime();
  this.alarm_ = new G_Alarm(BindToObject(this.repaint, this), 
                            this.initPeriodMS_);
}

/**
 * Called periodically to repaint the canvas
 */
PROT_PhishMsgCanvasRepainter.prototype.repaint = function() {
  this.repaintFunc_();

  var nextRepaint;
  // If we're in "steady state", use the slow repaint rate, else fast
  if ((new Date).getTime() - this.startMS_ > this.steadyStateAtMS_)
    nextRepaint = this.steadyStatePeriodMS_;
  else 
    nextRepaint = this.initPeriodMS_;

  if (!((new Date).getTime() - this.startMS_ > this.quitAtMS_))
    this.alarm_ = new G_Alarm(BindToObject(this.repaint, this), nextRepaint);
}

/**
 * Called to stop repainting the canvas
 */
PROT_PhishMsgCanvasRepainter.prototype.cancel = function() {
  if (this.alarm_) {
    this.alarm_.cancel();
    this.alarm_ = null;
  }
  this.repaintFunc_ = null;
}
/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Google Safe Browsing.
 *
 * The Initial Developer of the Original Code is Google Inc.
 * Portions created by the Initial Developer are Copyright (C) 2006
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Fritz Schneider <fritz@google.com> (original author)
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */


// The warden checks request to see if they are for phishy pages. It
// does so by either querying a remote server with the URL (advanced
// protectoin mode) or querying our locally stored blacklists (privacy
// mode).
// 
// When the warden notices a problem, it queries all browser views
// (each of which corresopnds to an open browser window) to see
// whether one of them can handle it. A browser view can handle a
// problem if its browser window has an HTMLDocument loaded with the
// given URL and that Document hasn't already been flagged as a
// problem. For every problematic URL we notice loading, at most one
// Document is flagged as problematic. Otherwise you can get into
// trouble if multiple concurrent phishy pages load with the same URL.
//
// Since we check URLs very early in the request cycle (in a progress
// listener), the URL might not yet be associated with a Document when
// we determine that it is phishy. So the the warden retries finding
// a browser view to handle the problem until one can, or until it
// determines it should give up (see complicated logic below).
//
// The warden has displayers that the browser view uses to render
// different kinds of warnings (e.g., one that's shown before a page
// loads as opposed to one that's shown after the page has already
// loaded).
//
// Note: There is a single warden for the whole application.
//
// TODO better way to expose displayers/views to browser view

const kPhishWardenEnabledPref = "browser.safebrowsing.enabled";
const kPhishWardenRemoteLookups = "browser.safebrowsing.remoteLookups";

// We have hardcoded URLs that we let people navigate to in order to 
// check out the warning.
const kTestUrls = {
  "http://www.google.com/tools/firefox/safebrowsing/phish-o-rama.html": true,
  "http://www.mozilla.org/projects/bonecho/anti-phishing/its-a-trap.html": true,
  "http://www.mozilla.com/firefox/its-a-trap.html": true,
}

/**
 * Abtracts the checking of user/browser actions for signs of
 * phishing. 
 *
 * @param progressListener nsIDocNavStartProgressListener
 * @constructor
 */
function PROT_PhishingWarden(progressListener) {
  PROT_ListWarden.call(this);

  this.debugZone = "phishwarden";
  this.testing_ = false;
  this.browserViews_ = [];

  // Use this to query preferences
  this.prefs_ = new G_Preferences();

  // Only one displayer so far; perhaps we'll have others in the future
  this.displayers_ = {
    "afterload": PROT_PhishMsgDisplayer,
  };

  // We use this dude to do lookups on our remote server
  this.fetcher_ = new PROT_TRFetcher();

  // We need to know whether we're enabled and whether we're in advanced
  // mode, so reflect the appropriate preferences into our state.

  // Read state: should we be checking remote preferences?
  this.checkRemote_ = this.prefs_.getPref(kPhishWardenRemoteLookups, null);
  
  // true if we should use whitelists to suppress remote lookups
  this.checkWhitelists_ = false;

  // Get notifications when the remote check preference changes
  var checkRemotePrefObserver = BindToObject(this.onCheckRemotePrefChanged,
                                             this);
  this.prefs_.addObserver(kPhishWardenRemoteLookups, checkRemotePrefObserver);

  // Global preference to enable the phishing warden
  this.phishWardenEnabled_ = this.prefs_.getPref(kPhishWardenEnabledPref, null);

  // Get notifications when the phishing warden enabled pref changes
  var phishWardenPrefObserver = 
    BindToObject(this.onPhishWardenEnabledPrefChanged, this);
  this.prefs_.addObserver(kPhishWardenEnabledPref, phishWardenPrefObserver);
  
  // Get notifications when the data provider pref changes
  var dataProviderPrefObserver =
    BindToObject(this.onDataProviderPrefChanged, this);
  this.prefs_.addObserver(kDataProviderIdPref, dataProviderPrefObserver);

  // hook up our browser listener
  this.progressListener_ = progressListener;
  this.progressListener_.callback = this;
  this.progressListener_.enabled = this.phishWardenEnabled_;
  // ms to wait after a request has started before firing JS callback
  this.progressListener_.delay = 1500;

  // object to keep track of request errors if we're in remote check mode
  this.requestBackoff_ = new RequestBackoff(3 /* num errors */,
                                   10*60*1000 /* error time, 10min */,
                                   10*60*1000 /* backoff interval, 10min */,
                                   6*60*60*1000 /* max backoff, 6hr */);

  G_Debug(this, "phishWarden initialized");
}

PROT_PhishingWarden.inherits(PROT_ListWarden);

/**
 * We implement nsIWebProgressListener
 */
PROT_PhishingWarden.prototype.QueryInterface = function(iid) {
  if (iid.equals(Ci.nsISupports) || 
      iid.equals(Ci.nsIWebProgressListener) ||
      iid.equals(Ci.nsISupportsWeakReference))
    return this;
  throw Components.results.NS_ERROR_NO_INTERFACE;
}

/**
 * Cleanup on shutdown.
 */
PROT_PhishingWarden.prototype.shutdown = function() {
  this.progressListener_.callback = null;
  this.progressListener_ = null;
  this.listManager_ = null;
}

/**
 * When a preference (either advanced features or the phishwarden
 * enabled) changes, we might have to start or stop asking for updates. 
 * 
 * This is a little tricky; we start or stop management only when we
 * have complete information we can use to determine whether we
 * should.  It could be the case that one pref or the other isn't set
 * yet (e.g., they haven't opted in/out of advanced features). So do
 * nothing unless we have both pref values -- we get notifications for
 * both, so eventually we will start correctly.
 */ 
PROT_PhishingWarden.prototype.maybeToggleUpdateChecking = function() {
  if (this.testing_)
    return;

  var phishWardenEnabled = this.prefs_.getPref(kPhishWardenEnabledPref, null);

  this.checkRemote_ = this.prefs_.getPref(kPhishWardenRemoteLookups, null);

  G_Debug(this, "Maybe toggling update checking. " +
          "Warden enabled? " + phishWardenEnabled + " || " +
          "Check remote? " + this.checkRemote_);

  // Do nothing unless both prefs are set.  They can be null (unset), true, or
  // false.
  if (phishWardenEnabled === null || this.checkRemote_ === null)
    return;

  // We update and save to disk all tables if we don't have remote checking
  // enabled.
  if (phishWardenEnabled === true) {
    // If anti-phishing is enabled, we always download the local files to
    // use in case remote lookups fail.
    this.enableBlacklistTableUpdates();
    this.enableWhitelistTableUpdates();

    if (this.checkRemote_ === true) {
      // Remote lookup mode
      // We check to see if the local list update host is the same as the
      // remote lookup host.  If they are the same, then we don't bother
      // to do a remote url check if the url is in the whitelist.
      var ioService = Cc["@mozilla.org/network/io-service;1"]
                     .getService(Ci.nsIIOService);
      var updateHost = '';
      var lookupHost = '';
      try {
        var url = ioService.newURI(gDataProvider.getUpdateURL(),
                                         null, null);
        updateHost = url.asciiHost;
      } catch (e) { }
      try {
        var url = ioService.newURI(gDataProvider.getLookupURL(),
                                         null, null);
        lookupHost = url.asciiHost;
      } catch (e) { }

      if (updateHost && lookupHost && updateHost == lookupHost) {
        // The data provider for local lists and remote lookups is the
        // same, enable whitelist lookup suppression.
        this.checkWhitelists_ = true;
      } else {
        // hosts don't match, don't use whitelist suppression
        this.checkWhitelists_ = false;
      }
    }
  } else {
    // Anti-phishing is off, disable table updates
    this.disableBlacklistTableUpdates();
    this.disableWhitelistTableUpdates();
  }
}

/**
 * Controllers register their browser views with us
 *
 * @param view Reference to a browser view 
 */
PROT_PhishingWarden.prototype.addBrowserView = function(view) {
  G_Debug(this, "New browser view registered.");
  this.browserViews_.push(view);
}

/**
 * Controllers unregister their views when their window closes
 *
 * @param view Reference to a browser view 
 */
PROT_PhishingWarden.prototype.removeBrowserView = function(view) {
  for (var i = 0; i < this.browserViews_.length; i++)
    if (this.browserViews_[i] === view) {
      G_Debug(this, "Browser view unregistered.");
      this.browserViews_.splice(i, 1);
      return;
    }
  G_Assert(this, false, "Tried to unregister non-existent browser view!");
}

/**
 * Deal with a user changing the pref that says whether we should check
 * the remote server (i.e., whether we're in advanced mode)
 *
 * @param prefName Name of the pref holding the value indicating whether
 *                 we should check remote server
 */
PROT_PhishingWarden.prototype.onCheckRemotePrefChanged = function(prefName) {
  this.checkRemote_ = this.prefs_.getBoolPrefOrDefault(prefName,
                                                       this.checkRemote_);
  this.requestBackoff_.reset();
  this.maybeToggleUpdateChecking();
}

/**
 * Deal with a user changing the pref that says whether we should 
 * enable the phishing warden (i.e., that SafeBrowsing is active)
 *
 * @param prefName Name of the pref holding the value indicating whether
 *                 we should enable the phishing warden
 */
PROT_PhishingWarden.prototype.onPhishWardenEnabledPrefChanged = function(
                                                                    prefName) {
  this.phishWardenEnabled_ = 
    this.prefs_.getBoolPrefOrDefault(prefName, this.phishWardenEnabled_);
  this.requestBackoff_.reset();
  this.maybeToggleUpdateChecking();
  this.progressListener_.enabled = this.phishWardenEnabled_;
}

/**
 * Event fired when the user changes data providers.
 */
PROT_PhishingWarden.prototype.onDataProviderPrefChanged = function(prefName) {
  // We want to reset request backoff state since it's a different provider.
  this.requestBackoff_.reset();

  // If we have a new data provider and we're doing remote lookups, then
  // we may want to use whitelist lookup suppression or change which
  // tables are being downloaded.
  if (this.checkRemote_) {
    this.maybeToggleUpdateChecking();
  }
}

/**
 * A request for a Document has been initiated somewhere. Check it!
 *
 * @param request
 * @param url
 */ 
PROT_PhishingWarden.prototype.onDocNavStart = function(request, url) {
  G_Debug(this, "checkRemote: " +
          (this.checkRemote_ ? "yes" : "no"));

  // If we're on a test page, trigger the warning.
  // XXX Do we still need a test url or should each provider just put
  // it in their local list?
  if (this.isBlacklistTestURL(url)) {
    this.houstonWeHaveAProblem_(request);
    return;
  }

  // Make a remote lookup check if the pref is selected and if we haven't
  // triggered server backoff.  Otherwise, make a local check.
  if (this.checkRemote_ && this.requestBackoff_.canMakeRequest()) {
    // If we can use whitelists to suppress remote lookups, do so.
    if (this.checkWhitelists_) {
      var maybeRemoteCheck = BindToObject(this.maybeMakeRemoteCheck_,
                                          this,
                                          url,
                                          request);
      this.isWhiteURL(url, maybeRemoteCheck);
    } else {
      // Do a remote lookup (don't check whitelists)
      this.fetcher_.get(url,
                        BindToObject(this.onTRFetchComplete,
                                     this,
                                     url,
                                     request));
    }
  } else {
    // Check the local lists for a match.
    var evilCallback = BindToObject(this.localListMatch_,
                                    this,
                                    url,
                                    request);
    this.isEvilURL(url, evilCallback);
  }
}

/** 
 * Callback from whitelist check when remote lookups is on.
 * @param url String url to lookup
 * @param request nsIRequest object
 * @param status int enum from callback (PROT_ListWarden.IN_BLACKLIST,
 *    PROT_ListWarden.IN_WHITELIST, PROT_ListWarden.NOT_FOUND)
 */
PROT_PhishingWarden.prototype.maybeMakeRemoteCheck_ = function(url, request, status) {
  if (PROT_ListWarden.IN_WHITELIST == status)
    return;

  G_Debug(this, "Local whitelist lookup failed");
  this.fetcher_.get(url,
                    BindToObject(this.onTRFetchComplete,
                                 this,
                                 url,
                                 request));
}

/**
 * Invoked with the result of a lookupserver request.
 *
 * @param url String the URL we looked up
 * @param request The nsIRequest in which we're interested
 * @param trValues Object holding name/value pairs parsed from the
 *                 lookupserver's response
 * @param status Number HTTP status code or NS_ERROR_NOT_AVAILABLE if there's
 *               an HTTP error
 */
PROT_PhishingWarden.prototype.onTRFetchComplete = function(url,
                                                           request,
                                                           trValues,
                                                           status) {
  // Did the remote http request succeed?  If not, we fall back on
  // local lists.
  if (status == Components.results.NS_ERROR_NOT_AVAILABLE ||
      this.requestBackoff_.isErrorStatus_(status)) {
    this.requestBackoff_.noteServerResponse(status);

    G_Debug(this, "remote check failed, using local lists instead");
    var evilCallback = BindToObject(this.localListMatch_,
                                    this,
                                    url,
                                    request);
    this.isEvilURL(url, evilCallback);
  } else {
    var callback = BindToObject(this.houstonWeHaveAProblem_, this, request);
    this.checkRemoteData(callback, trValues);
  }
}

/**
 * One of our Check* methods found a problem with a request. Why do we
 * need to keep the nsIRequest (instead of just passing in the URL)? 
 * Because we need to know when to stop looking for the URL its
 * fetching, and to know this we need the nsIRequest.isPending flag.
 *
 * @param request nsIRequest that is problematic
 */
PROT_PhishingWarden.prototype.houstonWeHaveAProblem_ = function(request) {

  // We have a problem request that might or might not be associated
  // with a Document that's currently in a browser. If it is, we 
  // want that Document. If it's not, we want to give it a chance to 
  // be loaded. See below for complete details.

  if (this.maybeLocateProblem_(request))       // Cases 1 and 2 (see below)
    return;

  // OK, so the request isn't associated with any currently accessible
  // Document, and we want to give it the chance to be. We don't want
  // to retry forever (e.g., what if the Document was already displayed
  // and navigated away from?), so we'll use nsIRequest.isPending to help
  // us decide what to do.
  //
  // A complication arises because there is a lag between when a
  // request transitions from pending to not-pending and when it's
  // associated with a Document in a browser. The transition from
  // pending to not occurs just before the notification corresponding
  // to NavWatcher.DOCNAVSTART (see NavWatcher), but the association
  // occurs afterwards. Unfortunately, we're probably in DOCNAVSTART.
  // 
  // Diagnosis by Darin:
  // ---------------------------------------------------------------------------
  // Here's a summary of what happens:
  //
  //   RestorePresentation() {
  //     Dispatch_OnStateChange(dummy_request, STATE_START)
  //     PostCompletionEvent()
  //   }
  //
  //   CompletionEvent() {
  //     ReallyRestorePresentation()
  //     Dispatch_OnStateChange(dummy_request, STATE_STOP)
  //   }
  //
  // So, now your code receives that initial OnStateChange event and sees
  // that the dummy_request is not pending and not loaded in any window.
  // So, you put a timeout(0) event in the queue.  Then, the CompletionEvent
  // is added to the queue.  The stack unwinds....
  //
  // Your timeout runs, and you find that the dummy_request is still not
  // pending and not loaded in any window.  Then the CompletionEvent
  // runs, and it hooks up the cached presentation.
  // 
  // https://bugzilla.mozilla.org/show_bug.cgi?id=319527
  // ---------------------------------------------------------------------------
  //
  // So the logic is:
  //
  //         request     found an unhandled          
  //  case   pending?    doc with the url?         action
  //  ----------------------------------------------------------------
  //   1      yes             yes           Use that doc (handled above)
  //   2      no              yes           Use that doc (handled above)
  //   3      yes             no            Retry
  //   4      no              no            Retry twice (case described above)
  //
  // We don't get into trouble with Docs with the same URL "stealing" the 
  // warning because there is exactly one warning signaled per nav to 
  // a problem URL, and each Doc can be marked as problematic at most once.

  if (request.isPending()) {        // Case 3

    G_Debug(this, "Can't find problem Doc; Req pending. Retrying.");
    new G_Alarm(BindToObject(this.houstonWeHaveAProblem_, 
                             this, 
                             request), 
                200 /*ms*/);

  } else {                          // Case 4

    G_Debug(this, 
            "Can't find problem Doc; Req completed. Retrying at most twice.");
    new G_ConditionalAlarm(BindToObject(this.maybeLocateProblem_, 
                                        this, 
                                        request),
                           0 /* next event loop */, 
                           true /* repeat */, 
                           2 /* at most twice */);
  }
}

/**
 * Query all browser views we know about and offer them the chance to
 * handle the problematic request.
 *
 * @param request nsIRequest that is problematic
 * 
 * @returns Boolean indicating if someone decided to handle it
 */
PROT_PhishingWarden.prototype.maybeLocateProblem_ = function(request) {
  G_Debug(this, "Trying to find the problem.");

  G_Debug(this, this.browserViews_.length + " browser views to check.");
  for (var i = 0; i < this.browserViews_.length; i++) {
    if (this.browserViews_[i].tryToHandleProblemRequest(this, request)) {
      G_Debug(this, "Found browser view willing to handle problem!");
      return true;
    }
    G_Debug(this, "wrong browser view");
  }
  return false;
}

/**
 * Indicates if this URL is one of the possible blacklist test URLs.
 * These test URLs should always be considered as phishy.
 *
 * @param url URL to check 
 * @return A boolean indicating whether this is one of our blacklist
 *         test URLs
 */
PROT_PhishingWarden.prototype.isBlacklistTestURL = function(url) {
  // Explicitly check for URL so we don't get JS warnings in strict mode.
  if (kTestUrls[url])
    return true;
  return false;
}

/**
 * Callback for found local blacklist match.  First we report that we have
 * a blacklist hit, then we bring up the warning dialog.
 * @param status Number enum from callback (PROT_ListWarden.IN_BLACKLIST,
 *    PROT_ListWarden.IN_WHITELIST, PROT_ListWarden.NOT_FOUND)
 */
PROT_PhishingWarden.prototype.localListMatch_ = function(url, request, status) {
  if (PROT_ListWarden.IN_BLACKLIST != status)
    return;

  // Maybe send a report
  (new PROT_Reporter).report("phishblhit", url);
  this.houstonWeHaveAProblem_(request);
}

/**
 * Examine data fetched from a lookup server for evidence of a
 * phishing problem. 
 *
 * @param callback Function to invoke if there is a problem. 
 * @param trValues Object containing name/value pairs the server returned
 */
PROT_PhishingWarden.prototype.checkRemoteData = function(callback, 
                                                         trValues) {

  if (!trValues) {
    G_Debug(this, "Didn't get TR values from the server.");
    return;
  }
  
  G_Debug(this, "Page has phishiness " + trValues["phishy"]);

  if (trValues["phishy"] == 1) {     // It's on our blacklist 
    G_Debug(this, "Remote blacklist hit");
    callback(this);
  } else {
    G_Debug(this, "Remote blacklist miss");
  }
}

/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Google Safe Browsing.
 *
 * The Initial Developer of the Original Code is Google Inc.
 * Portions created by the Initial Developer are Copyright (C) 2006
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Fritz Schneider <fritz@google.com> (original author)
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */


// A tiny class to do reporting for us. We report interesting user actions
// such as the user hitting a blacklisted page, and the user accepting
// or declining the warning.
//
// Each report has a subject and data. Current reports are:
//
// subject         data     meaning
// --------------------------------
// phishnavaway    url      the user navigated away from a phishy page
// phishdecline    url      the user declined our warning
// phishaccept     url      the user accepted our warning
// phishblhit      url      the user loaded a phishing page
//
// We only send reports in advanced protection mode, and even then we
// strip cookies from the request before sending it.

/**
 * A very complicated class to send pings to the provider. The class does
 * nothing if we're not in advanced protection mode.
 *
 * @constructor
 */
function PROT_Reporter() {
  this.debugZone = "reporter";
  this.prefs_ = new G_Preferences();
}

/**
 * Send a report!
 *
 * @param subject String indicating what this report is about (will be 
 *                urlencoded)
 * @param data String giving extra information about this report (will be 
 *                urlencoded)
 */
PROT_Reporter.prototype.report = function(subject, data) {
  // Send a report iff we're in advanced protection mode
  if (!this.prefs_.getPref(kPhishWardenRemoteLookups, false))
    return;
  // Make sure a report url is defined
  var url = gDataProvider.getReportURL();

  // Report url is optional, so we just ignore the request if a report
  // url isn't provided.
  if (!url)
    return;

  url += "evts=" + encodeURIComponent(subject)
         + "&evtd=" + encodeURIComponent(data);
  G_Debug(this, "Sending report: " + url);
  (new PROT_XMLFetcher(true /* strip cookies */)).get(url, null /* no cb */);
}
/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Google Safe Browsing.
 *
 * The Initial Developer of the Original Code is Google Inc.
 * Portions created by the Initial Developer are Copyright (C) 2006
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Fritz Schneider <fritz@google.com> (original author)
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */


// A helper class that does "trustrank" lookups on a remote
// server. Right now this lookup just indicates if a page is
// phishing. The response format is protocol4 name/value pairs.
// 
// Since we're sending full URLs to the server, we try to encrypt
// them before transmission. Else HTTPS query params could leak.

/**
 * A helper class that fetches trustrank values, parses them, and
 * passes them via an object to a callback.
  * 
 * @constructor
 */
function PROT_TRFetcher(opt_noCrypto) {
  this.debugZone = "trfetcher";
  this.useCrypto_ = !opt_noCrypto;
  this.protocol4Parser_ = new G_Protocol4Parser();

  // We lazily instantiate the UrlCrypto object due to:
  // https://bugzilla.mozilla.org/show_bug.cgi?id=321024
  //
  // Otherwise here we would use:
  // this.urlCrypto_ = new PROT_UrlCrypto();
}

PROT_TRFetcher.TRY_REKEYING_RESPONSE = "pleaserekey";

/**
 * Get the URL of the request that will fetch us TR for the argument URL
 *
 * @param url String containing the URL we'd like to fetch info about
 *
 * @returns String containing the url we should use to fetch tr info
 */
PROT_TRFetcher.prototype.getRequestURL_ = function(url) {

  if (!this.urlCrypto_)
    this.urlCrypto_ = new PROT_UrlCrypto();

  G_Debug(this, "Fetching for " + url);
    
  var requestURL = gDataProvider.getLookupURL();
  if (!requestURL)
    return null;

  if (this.useCrypto_) {
    var maybeCryptedParams = this.urlCrypto_.maybeCryptParams({ "q": url});
    
    for (var param in maybeCryptedParams) 
      requestURL += param + "=" + 
                    encodeURIComponent(maybeCryptedParams[param]) + "&";
  } else {
    requestURL += "q=" + encodeURIComponent(url);
  }

  G_Debug(this, "Request URL: " + requestURL);

  return requestURL;
};

/**
 * Fetches information about a page.
 * 
 * @param forPage URL for which to fetch info
 *
 * @param callback Function to call back when complete.
 */
PROT_TRFetcher.prototype.get = function(forPage, callback) {
  
  var url = this.getRequestURL_(forPage);
  if (!url) {
    G_Debug(this, "No remote lookup url.");
    return;
  }
  var closure = BindToObject(this.onFetchComplete_, this, callback);
  (new PROT_XMLFetcher()).get(url, closure);
};

/**
 * Invoked when a fetch has completed.
 *
 * @param callback Function to invoke with parsed response object
 * @param responseText Text of the protocol4 message
 * @param httpStatus Number HTTP status code or NS_ERROR_NOT_AVAILABLE if the
 *     request failed
 */
PROT_TRFetcher.prototype.onFetchComplete_ = function(callback, responseText,
                                                     httpStatus) {
  
  var responseObj = this.extractResponse_(responseText);

  // The server might tell us to rekey, for example if it sees that
  // our request was unencrypted (meaning that we might not yet have
  // a key). If so, pass this hint along to the crypto key manager.

  if (responseObj[PROT_TRFetcher.TRY_REKEYING_RESPONSE] == "1" &&
      this.urlCrypto_) {
    G_Debug(this, "We're supposed to re-key. Trying.");
    var manager = this.urlCrypto_.getManager();
    if (manager)
      manager.maybeReKey();
  }

  G_Debug(this, "TR Response:");
  for (var field in responseObj)
    G_Debug(this, field + "=" + responseObj[field]);

  callback(responseObj, httpStatus);
};

/**
 * Parse a protocol4 message (lookup server response)
 * 
 * @param responseText String containing the server's response
 *
 * @returns Object containing the returned values or null if no
 *          response was received
 */
PROT_TRFetcher.prototype.extractResponse_ = function(responseText) {
  return this.protocol4Parser_.parse(responseText);
};

//@line 27 "/usr/obj/ports/mozilla-firefox-2.0.0.12/mozilla/browser/components/safebrowsing/src/nsSafebrowsingApplication.js"

var modScope = this;
function Init() {
  var jslib = Cc["@mozilla.org/url-classifier/jslib;1"]
              .getService().wrappedJSObject;
  modScope.String.prototype.startsWith = jslib.String.prototype.startsWith;
  modScope.G_Debug = jslib.G_Debug;
  modScope.G_Assert = jslib.G_Assert;
  modScope.G_Alarm = jslib.G_Alarm;
  modScope.G_ConditionalAlarm = jslib.G_ConditionalAlarm;
  modScope.G_ObserverWrapper = jslib.G_ObserverWrapper;
  modScope.G_Preferences = jslib.G_Preferences;
  modScope.PROT_XMLFetcher = jslib.PROT_XMLFetcher;
  modScope.BindToObject = jslib.BindToObject;
  modScope.G_Protocol4Parser = jslib.G_Protocol4Parser;
  modScope.G_ObjectSafeMap = jslib.G_ObjectSafeMap;
  modScope.PROT_UrlCrypto = jslib.PROT_UrlCrypto;
  modScope.RequestBackoff = jslib.RequestBackoff;
  
  // We only need to call Init once
  modScope.Init = function() {};
}

// Module object
function SafebrowsingApplicationMod() {
  this.firstTime = true;
  this.cid = Components.ID("{c64d0bcb-8270-4ca7-a0b3-3380c8ffecb5}");
  this.progid = "@mozilla.org/safebrowsing/application;1";
}

SafebrowsingApplicationMod.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,
                                  "Safebrowsing Application Module",
                                  this.progid,
                                  fileSpec,
                                  loc,
                                  type);
};

SafebrowsingApplicationMod.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;
}

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

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

var ApplicationModInst = new SafebrowsingApplicationMod();

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