// LOG LEVELS
let VERB=1;
let DBUG=2;
let INFO=3;
let NOTE=4;
let WARN=5;

// PREFERENCE BRANCHES
let PREFBRANCH_ROOT=0;
let PREFBRANCH_RULE_TOGGLE=1;
let PREFBRANCH_NONE=2;

// maps domain patterns (with at most one  wildcard) to RuleSets
let https_domains = {};
// URLs we've given up on rewriting because of redirection loops
let https_everywhere_blacklist = {};
// domains for which there is at least one blacklisted URL
let https_blacklist_domains = {};

const CI = Components.interfaces;
const CC = Components.classes;
const Ci = Components.interfaces;
const Cc = Components.classes;
const Cu = Components.utils;

Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/FileUtils.jsm");

const SERVICE_CTRID = "@eff.org/https-everywhere;1";
const SERVICE_ID=Components.ID("{32c165b4-fe5e-4964-9250-603c410631b4}");
const SERVICE_NAME = "Encrypts your communications with a number of major websites";

const LLVAR = "LogLevel";

const IOS = CC["@mozilla.org/network/io-service;1"].getService(CI.nsIIOService);
const OS = CC['@mozilla.org/observer-service;1'].getService(CI.nsIObserverService);
const LOADER = CC["@mozilla.org/moz/jssubscript-loader;1"].getService(CI.mozIJSSubScriptLoader);
const _INCLUDED = {};

// NoScript uses this blob to include js constructs that stored in the chrome/
// directory, but are not attached to the Firefox UI (normally, js located
// there is attached to an Overlay and therefore is part of the UI).

// Reasons for this: things in components/ directory cannot be split into
// separate files; things in chrome/ can be

const INCLUDE = function(name) {
  if (arguments.length > 1)
    for (var j = 0, len = arguments.length; j < len; j++)
      INCLUDE(arguments[j]);
  else if (!_INCLUDED[name]) {
    // we used to try/catch here, but that was less useful because it didn't
    // produce line numbers for syntax errors
    LOADER.loadSubScript("chrome://https-everywhere/content/code/"
            + name + ".js");
    _INCLUDED[name] = true;
  }
};

const WP_STATE_START = CI.nsIWebProgressListener.STATE_START;
const WP_STATE_STOP = CI.nsIWebProgressListener.STATE_STOP;
const WP_STATE_DOC = CI.nsIWebProgressListener.STATE_IS_DOCUMENT;
const WP_STATE_START_DOC = WP_STATE_START | WP_STATE_DOC;
const WP_STATE_RESTORING = CI.nsIWebProgressListener.STATE_RESTORING;

const LF_VALIDATE_ALWAYS = CI.nsIRequest.VALIDATE_ALWAYS;
const LF_LOAD_BYPASS_ALL_CACHES = CI.nsIRequest.LOAD_BYPASS_CACHE | CI.nsICachingChannel.LOAD_BYPASS_LOCAL_CACHE;

const NS_OK = 0;
const NS_BINDING_ABORTED = 0x804b0002;
const NS_BINDING_REDIRECTED = 0x804b0003;
const NS_ERROR_UNKNOWN_HOST = 0x804b001e;
const NS_ERROR_REDIRECT_LOOP = 0x804b001f;
const NS_ERROR_CONNECTION_REFUSED = 0x804b000e;
const NS_ERROR_NOT_AVAILABLE = 0x804b0111;

const LOG_CONTENT_BLOCK = 1;
const LOG_CONTENT_CALL = 2;
const LOG_CONTENT_INTERCEPT = 4;
const LOG_CHROME_WIN = 8;
const LOG_XSS_FILTER = 16;
const LOG_INJECTION_CHECK = 32;
const LOG_DOM = 64;
const LOG_JS = 128;
const LOG_LEAKS = 1024;
const LOG_SNIFF = 2048;
const LOG_CLEARCLICK = 4096;

const HTML_NS = "http://www.w3.org/1999/xhtml";

const WHERE_UNTRUSTED = 1;
const WHERE_TRUSTED = 2;
const ANYWHERE = 3;

const N_COHORTS = 1000; 

const DUMMY_OBJ = {};
DUMMY_OBJ.wrappedJSObject = DUMMY_OBJ;
const DUMMY_FUNC = function() {};
const DUMMY_ARRAY = [];

const EARLY_VERSION_CHECK = !("nsISessionStore" in CI && typeof(/ /) === "object");

// This is probably obsolete since the switch to the channel.redirectTo API
const OBSERVER_TOPIC_URI_REWRITE = "https-everywhere-uri-rewrite";

function xpcom_checkInterfaces(iid,iids,ex) {
  for (var j = iids.length; j-- >0;) {
    if (iid.equals(iids[j])) return true;
  }
  throw ex;
}

INCLUDE('IOUtil', 'HTTPSRules', 'HTTPS', 'ApplicableList');

Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");

function HTTPSEverywhere() {

  // Set up logging in each component:
  HTTPS.log = HTTPSRules.log = RuleWriter.log = this.log = https_everywhereLog;

  this.expandoMap = new WeakMap();

  this.log = https_everywhereLog;
  this.wrappedJSObject = this;
  this.https_rules = HTTPSRules;
  this.rw = RuleWriter;    // currently used for some file IO helpers, though that
                           // should probably be refactored
  this.INCLUDE=INCLUDE;
  this.ApplicableList = ApplicableList;
  this.browser_initialised = false; // the browser is completely loaded


  this.prefs = this.get_prefs();
  this.rule_toggle_prefs = this.get_prefs(PREFBRANCH_RULE_TOGGLE);

  this.httpNowhereEnabled = this.prefs.getBoolPref("http_nowhere.enabled");
  this.isMobile = this.doMobileCheck();

  // We need to use observers instead of categories for FF3.0 for these:
  // https://developer.mozilla.org/en/Observer_Notifications
  // https://developer.mozilla.org/en/nsIObserverService.
  // https://developer.mozilla.org/en/nsIObserver
  // We also use the observer service to let other extensions know about URIs
  // we rewrite.
  this.obsService = CC["@mozilla.org/observer-service;1"]
                    .getService(Components.interfaces.nsIObserverService);
                    
  if (this.prefs.getBoolPref("globalEnabled")) {
    this.obsService.addObserver(this, "profile-before-change", false);
    this.obsService.addObserver(this, "profile-after-change", false);
    this.obsService.addObserver(this, "sessionstore-windows-restored", false);
    this.obsService.addObserver(this, "browser:purge-session-history", false);
  } else {
    // Need this to initialize FF for Android UI even when HTTPS-E is off
    if (this.isMobile) {
      this.obsService.addObserver(this, "sessionstore-windows-restored", false);
    }
  }

  var pref_service = Components.classes["@mozilla.org/preferences-service;1"]
      .getService(Components.interfaces.nsIPrefBranchInternal);
  var branch = pref_service.QueryInterface(Components.interfaces.nsIPrefBranchInternal);

  branch.addObserver("extensions.https_everywhere.enable_mixed_rulesets",
                         this, false);
  branch.addObserver("security.mixed_content.block_active_content",
                         this, false);

  return;
}


/*
In recent versions of Firefox and HTTPS Everywhere, the call stack for performing an HTTP -> HTTPS rewrite looks like this:

1. HTTPSEverywhere.observe() gets a callback with the "http-on-modify-request" topic, and the channel as a subject

1. HTTPSEverywhere.shouldIgnoreURI() checks for very quick reasons to ignore a
request, such as redirection loops, non-HTTP[S] URIs, and OCSP

    2. HTTPS.replaceChannel() 

       3. HTTPSRules.rewrittenURI() 
            
           4. HTTPSRules.potentiallyApplicableRulesets uses <target host=""> elements to identify relevant rulesets

           foreach RuleSet:

               4. RuleSet.transformURI()

                   5. RuleSet.apply() does the tests and rewrites with RegExps, returning a string

               4. RuleSet.transformURI() makes a new uri object for the destination string, if required

    2. HTTPS.replaceChannel() calls channel.redirectTo() if a redirect is needed


In addition, the following other important tasks happen along the way:

HTTPSEverywhere.observe()    finds a reference to the ApplicableList or alist that represents the toolbar context menu

HTTPS.replaceChannel()       notices redirect loops (and used to do much more complex XPCOM API work in the NoScript-based past)

HTTPSRules.rewrittenURI()    works around weird URI types like about: and http://user:pass@example.com/
                             and notifies the alist of what it should display for each ruleset

*/

// This defines for Mozilla what stuff HTTPSEverywhere will implement.

// ChannelEventSink used to be necessary in order to handle redirects (eg
// HTTP redirects) correctly.  It may now be obsolete? XXX

HTTPSEverywhere.prototype = {
  prefs: null,
  // properties required for XPCOM registration:
  classDescription: SERVICE_NAME,
  classID:          SERVICE_ID,
  contractID:       SERVICE_CTRID,

  _xpcom_factory: {
    createInstance: function (outer, iid) {
      if (outer != null)
        throw Components.results.NS_ERROR_NO_AGGREGATION;
      if (!HTTPSEverywhere.instance)
        HTTPSEverywhere.instance = new HTTPSEverywhere();
      return HTTPSEverywhere.instance.QueryInterface(iid);
    },

    QueryInterface: XPCOMUtils.generateQI(
      [ Components.interfaces.nsISupports,
        Components.interfaces.nsIModule,
        Components.interfaces.nsIFactory ])
  },

  // [optional] an array of categories to register this component in.
  _xpcom_categories: [
    {
      category: "app-startup",
    }
  ],

  // QueryInterface implementation, e.g. using the generateQI helper
  QueryInterface: XPCOMUtils.generateQI(
    [ Components.interfaces.nsIObserver,
      Components.interfaces.nsISupports,
      Components.interfaces.nsISupportsWeakReference,
      Components.interfaces.nsIChannelEventSink ]),

  wrappedJSObject: null,  // Initialized by constructor

  getWeakReference: function () {
    return Components.utils.getWeakReference(this);
  },

  // An "expando" is an attribute glued onto something.  From NoScript.
  getExpando: function(browser, key) {
    let obj = this.expandoMap.get(browser);
    if (!obj) {
      if (browser.currentURI) {
        this.log(NOTE, "No expando for " + browser.currentURI.spec);
      }
      return null;
    }
    return obj[key];
  },

  setExpando: function(browser, key, value) {
    if (!this.expandoMap.has(browser)) {
      this.expandoMap.set(browser, {});
    }
    let obj = this.expandoMap.get(browser);
    obj[key] = value;
  },

  // We use resetApplicableList to make a fresh list of rulesets that could have
  // applied to the content in the current page (the "applicable list" is used
  // for the context menu in the UI).  This will be appended to as various
  // content is embedded / requested by JavaScript.
  resetApplicableList: function(browser) {
    if (!this.prefs.getBoolPref("globalEnabled")) {
      return;
    }
    try {
      this.newApplicableListForBrowser(browser);
    } catch (e) {
      this.log(WARN, "Couldn't make applicable list"+e);
    }
  },

  // Given an nsIChannel (essentially, a container for an HTTP or similar
  // resource request), try to find the relevant tab if there is one.
  // Specifically, find the XUL <browser> element for that tab. Note
  // there are multiple meanings for the word 'browser' in Firefox, described at:
  // https://developer.mozilla.org/en-US/Add-ons/Code_snippets/Tabbed_browser
  // We're looking for this one:
  // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/browser
  // Also note some requests, like Safe Browsing requests, will have no
  // associated tab.
  getBrowserForChannel: function(channel) {
    let loadContext, topFrameElement, associatedWindow;
    let spec = channel.URI.spec;
    try {
      loadContext = channel.notificationCallbacks.getInterface(CI.nsILoadContext);
    } catch(e) {
    }

    if (!loadContext) {
      try {
        loadContext = channel.loadGroup.notificationCallbacks
          .getInterface(CI.nsILoadContext);
      } catch(e) {
        // Lots of requests have no notificationCallbacks, mostly background
        // ones like OCSP checks or smart browsing fetches.
        this.log(DBUG, "getBrowserForChannel: no loadContext for " + spec);
        return null;
      }
    }

    if (loadContext) {
      topFrameElement = loadContext.topFrameElement;
      try {
        // If loadContext is an nsDocShell, associatedWindow is present.
        // Otherwise, if it's just a LoadContext, accessing it will throw
        // NS_ERROR_UNEXPECTED.
        associatedWindow = loadContext.associatedWindow;
      } catch (e) {
      }
    }

    // On e10s (multiprocess, aka electrolysis) Firefox,
    // loadContext.topFrameElement gives us a reference to the XUL <browser>
    // element we need. However, on non-e10s Firefox, topFrameElement is null.
    if (topFrameElement) {
      return topFrameElement;
    } else if (associatedWindow) {
      // For non-e10s Firefox, get the XUL <browser> element using this rather
      // magical / opaque code cribbed from
      // https://developer.mozilla.org/en-US/Add-ons/Code_snippets/Tabbed_browser#Getting_the_browser_that_fires_the_http-on-modify-request_notification_(example_code_updated_for_loadContext)

      // this is the HTML DOM window of the page that just loaded
      var contentWindow = loadContext.associatedWindow;
      // aDOMWindow this is the firefox window holding the tab
      var aDOMWindow = contentWindow.top.QueryInterface(Ci.nsIInterfaceRequestor)
        .getInterface(Ci.nsIWebNavigation)
        .QueryInterface(Ci.nsIDocShellTreeItem)
        .rootTreeItem
        .QueryInterface(Ci.nsIInterfaceRequestor)
        .getInterface(Ci.nsIDOMWindow);
      // this is the gBrowser object of the firefox window this tab is in
      var gBrowser = aDOMWindow.gBrowser;
      if (gBrowser && gBrowser._getTabForContentWindow) {
        var aTab = gBrowser._getTabForContentWindow(contentWindow.top);
        // this is the clickable tab xul element, the one found in the tab strip
        // of the firefox window, aTab.linkedBrowser is same as browser var above
        // this is the browser within the tab
        if (aTab) {
          return aTab.linkedBrowser;
        } else {
          this.log(NOTE, "getBrowserForChannel: aTab was null for " + spec);
          return null;
        }
      } else if (aDOMWindow.BrowserApp) {
        // gBrowser is unavailable in Firefox for Android, and in some desktop
        // contexts, like the fetches for new tab tiles (which have an
        // associatedWindow, but no gBrowser)?
        // If available, try using the BrowserApp API:
        // https://developer.mozilla.org/en-US/Add-ons/Firefox_for_Android/API/BrowserApp
        // TODO: We don't get the toolbar icon on android. Probably need to fix
        // the gBrowser reference in toolbar_button.js.
        // Also TODO: Where are these log messages going? They don't show up in
        // remote debug console.
        var mTab = aDOMWindow.BrowserApp.getTabForWindow(contentWindow.top);
        if (mTab) {
          return mTab.browser;
        } else {
          this.log(WARN, "getBrowserForChannel: mTab was null for " + spec);
          return null;
        }
      } else {
        this.log(INFO, "getBrowserForChannel: No gBrowser and no BrowserApp for " + spec);
        return null;
      }
    } else {
      this.log(NOTE, "getBrowserForChannel: No loadContext for " + spec);
      return null;
    }
  },

  // the lists get made when the urlbar is loading something new, but they
  // need to be appended to with reference only to the channel
  getApplicableListForChannel: function(channel) {
    var browser = this.getBrowserForChannel(channel);
    return this.getApplicableListForBrowser(browser);
  },

  newApplicableListForBrowser: function(browser) {
    if (!browser) {
      this.log(WARN, "Get alist without browser");
      return null;
    }
    var alist = new ApplicableList(this.log, browser.currentURI);
    this.setExpando(browser,"applicable_rules",alist);
    return alist;
  },

  getApplicableListForBrowser: function(browser) {
    if (!browser) {
      //this.log(WARN, "Get alist without browser");
      return null;
    }
    var alist= this.getExpando(browser,"applicable_rules");
    if (alist) {
      return alist;
    } else {
      alist = new ApplicableList(this.log, browser.currentURI);
      this.setExpando(browser,"applicable_rules",alist);
    }
    return alist;
  },

  // These are the highest level heuristics for figuring out whether
  // we should consider rewriting a URI.  Everything here should be simple
  // and avoid dependence on the ruleset library
  shouldIgnoreURI: function(channel, alist) {
    var uri = channel.URI;
    // Ignore all non-http(s) requests?
    if (!(uri.schemeIs("http") || uri.schemeIs("https"))) { return true; }

    // If HTTP Nowhere is enabled, skip the rest of the shouldIgnoreURI checks
    if (this.httpNowhereEnabled) {
      return false;
    }

    // These are URIs we've seen redirecting back in loops after we redirect them
    if (uri.spec in https_everywhere_blacklist) {
        this.log(DBUG, "Avoiding blacklisted " + uri.spec);
        if (alist) {
          alist.breaking_rule(https_everywhere_blacklist[uri.spec]);
        } else {
          this.log(NOTE,"Failed to indicate breakage in content menu");
        }
        return true;
    }

    // OCSP (currently) needs to be HTTP to avoid cert validation loops
    // though someone should rev the spec to allow opportunistic encryption
    if ("allowSTS" in channel) {
      // Firefox 32+ lets us infer whether this is an OCSP request
      if (!channel.allowSTS) {
        this.log(INFO, "Channel with HTTPS rewrites forbidden, deeming OCSP, for " + channel.URI.spec);
        return true;
      }
    } else {
      // Firefox <32 requires a more hacky estimate
      // load the list opportunistically to speed startup & FF 32+
      if (this.ocspList == undefined) { this.loadOCSPList(); }
      if (this.ocspList.indexOf(uri.spec.replace(/\/$/,'')) !== -1) {
        this.log(INFO, "Known ocsp request "+uri.spec);
        return true;
      }
    }

    return false;
  },

  loadOCSPList: function() {
    try {
      var loc = "chrome://https-everywhere/content/code/commonOCSP.json";
      var file = CC["@mozilla.org/file/local;1"].createInstance(CI.nsILocalFile);
      file.initWithPath(this.rw.chromeToPath(loc));
      var data = this.rw.read(file);
      this.ocspList = JSON.parse(data);
    } catch(e) {
      this.log(WARN, "Failed to load OCSP list: " + e);
      this.ocspList = [];
    }
  },

  observe: function(subject, topic, data) {
    // Top level glue for the nsIObserver API
    var channel = subject;
    //this.log(VERB,"Got observer topic: "+topic);

    if (topic == "http-on-modify-request") {
      if (!(channel instanceof CI.nsIHttpChannel)) return;

      this.log(DBUG,"Got http-on-modify-request: "+channel.URI.spec);
      // lst is null if no window is associated (ex: some XHR)
      var lst = this.getApplicableListForChannel(channel);
      if (this.shouldIgnoreURI(channel, lst)) return;
      HTTPS.replaceChannel(lst, channel, this.httpNowhereEnabled);
    } else if (topic == "http-on-examine-response") {
         this.log(DBUG, "Got http-on-examine-response @ "+ (channel.URI ? channel.URI.spec : '') );
         HTTPS.handleSecureCookies(channel);
    } else if (topic == "http-on-examine-merged-response") {
         this.log(DBUG, "Got http-on-examine-merged-response ");
         HTTPS.handleSecureCookies(channel);
    } else if (topic == "cookie-changed") {
      // Javascript can add cookies via document.cookie that are insecure.
      if (data == "added" || data == "changed") {
        // subject can also be an nsIArray! bleh.
        try {
          subject.QueryInterface(CI.nsIArray);
          var elems = subject.enumerate();
          while (elems.hasMoreElements()) {
            var cookie = elems.getNext()
                            .QueryInterface(CI.nsICookie2);
            if (!cookie.isSecure) {
              HTTPS.handleInsecureCookie(cookie);
            }
          }
        } catch(e) {
          subject.QueryInterface(CI.nsICookie2);
          if(!subject.isSecure) {
            HTTPS.handleInsecureCookie(subject);
          }
        }
      }
    } else if (topic == "profile-before-change") {
      this.log(INFO, "Got profile-before-change");
      var catman = Components.classes["@mozilla.org/categorymanager;1"]
           .getService(Components.interfaces.nsICategoryManager);
      catman.deleteCategoryEntry("net-channel-event-sinks", SERVICE_CTRID, true);
    } else if (topic == "profile-after-change") {
      this.log(DBUG, "Got profile-after-change");

      if(this.prefs.getBoolPref("globalEnabled")){
        OS.addObserver(this, "cookie-changed", false);
        OS.addObserver(this, "http-on-modify-request", false);
        OS.addObserver(this, "http-on-examine-merged-response", false);
        OS.addObserver(this, "http-on-examine-response", false);

        HTTPSRules.init();

        var catman = Components.classes["@mozilla.org/categorymanager;1"]
           .getService(Components.interfaces.nsICategoryManager);
        // hook on redirections (non persistent, otherwise crashes on 1.8.x)
        catman.addCategoryEntry("net-channel-event-sinks", SERVICE_CTRID,
      this.log(DBUG,"Got sessionstore-windows-restored");
      if (!this.isMobile) {
        this.maybeShowObservatoryPopup();
      } else {
        this.log(WARN, "Initializing Firefox for Android UI");
        Cu.import("chrome://https-everywhere/content/code/AndroidUI.jsm");
        AndroidUI.init();
      }
      this.browser_initialised = true;
    } else if (topic == "nsPref:changed") {
        // If the user toggles the Mixed Content Blocker settings, reload the rulesets
        // to enable/disable the mixedcontent ones

        // this pref gets set to false and then true during FF 26 startup!
        // so do nothing if we're being notified during startup
        if (!this.browser_initialised)
            return;
        switch (data) {
            case "security.mixed_content.block_active_content":
            case "extensions.https_everywhere.enable_mixed_rulesets":
                var p = CC["@mozilla.org/preferences-service;1"].getService(CI.nsIPrefBranch);
                var val = p.getBoolPref("security.mixed_content.block_active_content");
                this.log(INFO,"nsPref:changed for "+data + " to " + val);
                HTTPSRules.init();
                break;
        }
    } else if (topic == "browser:purge-session-history") {
      // The list of rulesets that have been loaded from the JSON DB
      // constitutes a parallel history store, so we have to clear it.
      this.log(DBUG, "History cleared, reloading HTTPSRules to avoid information leak.");
      HTTPSRules.init();
    }
    return;
  },

  maybeShowObservatoryPopup: function() {
    // Show the popup at most once.  Users who enabled the Observatory before
    // a version that would have shown it to them, don't need to see it
    // again.
    var ssl_observatory = CC["@eff.org/ssl-observatory;1"]
                      .getService(Components.interfaces.nsISupports)
                      .wrappedJSObject;
    var shown = ssl_observatory.myGetBoolPref("popup_shown");
    var enabled = ssl_observatory.myGetBoolPref("enabled");
    var that = this;
    var obs_popup_callback = function(result) {
      if (result) that.log(INFO, "Got positive proxy test.");
      else        that.log(INFO, "Got negative proxy text.");
      // We are now ready to show the popup in its most informative state
      that.chrome_opener("chrome://https-everywhere/content/observatory-popup.xul");
    };
    if (!shown && !enabled)
      ssl_observatory.registerProxyTestNotification(obs_popup_callback);

    if (shown && enabled)
      this.maybeCleanupObservatoryPrefs(ssl_observatory);
  },

  maybeCleanupObservatoryPrefs: function(ssl_observatory) {
    // Recover from a past UI processing bug that would leave the Observatory
    // accidentally disabled for some users
    // https://trac.torproject.org/projects/tor/ticket/10728
    var clean = ssl_observatory.myGetBoolPref("clean_config");
    if (clean) return;

    // unchanged: returns true if a pref has not been modified
    var unchanged = function(p){return !ssl_observatory.prefs.prefHasUserValue("extensions.https_everywhere._observatory."+p)};
    var cleanup_obsprefs_callback = function(tor_avail) {
      // we only run this once
      ssl_observatory.prefs.setBoolPref("extensions.https_everywhere._observatory.clean_config", true);
      if (!tor_avail) {
        // use_custom_proxy is the variable that is often false when it should be true;
        if (!ssl_observatory.myGetBoolPref("use_custom_proxy")) {
           // however don't do anything if any of the prefs have been set by the user
           if (unchanged("alt_roots") && unchanged("self_signed") && unchanged ("send_asn") && unchanged("priv_dns")) {
             ssl_observatory.prefs.setBoolPref("extensions.https_everywhere._observatory.use_custom_proxy", true);
           }
        }
      }
    }
    ssl_observatory.registerProxyTestNotification(cleanup_obsprefs_callback);
  },


  getExperimentalFeatureCohort: function() {
    // This variable is used for gradually turning on features for testing and
    // scalability purposes.  It is a random integer [0,N_COHORTS) generated
    // once and stored thereafter.
    // 
    // This is not currently used/called in the development branch
    var cohort;
    try {
      cohort = this.prefs.getIntPref("experimental_feature_cohort");
    } catch(e) {
      cohort = Math.round(Math.random() * N_COHORTS);
      this.prefs.setIntPref("experimental_feature_cohort", cohort);
    }
    return cohort;
  },

  // nsIChannelEventSink implementation
  // XXX This was here for rewrites in the past.  Do we still need it?
  onChannelRedirect: function(oldChannel, newChannel, flags) {  
    const uri = newChannel.URI;
    this.log(DBUG,"Got onChannelRedirect to "+uri.spec);
    if (!(newChannel instanceof CI.nsIHttpChannel)) {
      this.log(DBUG, newChannel + " is not an instance of nsIHttpChannel");
      return;
    }
    var alist = this.juggleApplicableListsDuringRedirection(oldChannel, newChannel);
    HTTPS.replaceChannel(alist, newChannel, this.httpNowhereEnabled);
  },

  juggleApplicableListsDuringRedirection: function(oldChannel, newChannel) {
    // If the new channel doesn't yet have a list of applicable rulesets, start
    // with the old one because that's probably a better representation of how
    // secure the load process was for this page
    var browser = this.getBrowserForChannel(oldChannel);
    var old_alist = null;
    if (browser) 
      old_alist = this.getExpando(browser,"applicable_rules");
    browser = this.getBrowserForChannel(newChannel);
    if (!browser) return null;
    var new_alist = this.getExpando(browser,"applicable_rules");
    if (old_alist && !new_alist) {
      new_alist = old_alist;
      this.setExpando(browser,"applicable_rules",new_alist);
    } else if (!new_alist) {
      new_alist = new ApplicableList(this.log, browser.currentURI);
      this.setExpando(browser,"applicable_rules",new_alist);
    }
    return new_alist;
  },

  asyncOnChannelRedirect: function(oldChannel, newChannel, flags, callback) {
        this.onChannelRedirect(oldChannel, newChannel, flags);
        callback.onRedirectVerifyCallback(0);
  },

  get_prefs: function(prefBranch) {
    if(!prefBranch) prefBranch = PREFBRANCH_ROOT;

    // get our preferences branch object
    // FIXME: Ugly hack stolen from https
    var branch_name;
    if(prefBranch === PREFBRANCH_RULE_TOGGLE)
      branch_name = "extensions.https_everywhere.rule_toggle.";
    else if (prefBranch === PREFBRANCH_NONE)
      branch_name = "";
    else
      branch_name = "extensions.https_everywhere.";
    var o_prefs = false;
    var o_branch = false;
    // this function needs to be called from inside https_everywhereLog, so
    // it needs to do its own logging...
    var econsole = Components.classes["@mozilla.org/consoleservice;1"]
      .getService(Components.interfaces.nsIConsoleService);

    o_prefs = Components.classes["@mozilla.org/preferences-service;1"]
                        .getService(Components.interfaces.nsIPrefService);

    if (!o_prefs)
    {
      econsole.logStringMessage("HTTPS Everywhere: Failed to get preferences-service!");
      return false;
    }

    o_branch = o_prefs.getBranch(branch_name);
    if (!o_branch)
    {
      econsole.logStringMessage("HTTPS Everywhere: Failed to get prefs branch!");
      return false;
    }

    if(prefBranch == PREFBRANCH_ROOT) {
      // make sure there's an entry for our log level
      try {
        o_branch.getIntPref(LLVAR);
      } catch (e) {
        econsole.logStringMessage("Creating new about:config https_everywhere.LogLevel variable");
        o_branch.setIntPref(LLVAR, WARN);
      }
    }

    return o_branch;
  },

  // Are we on Firefox for Android?
  doMobileCheck: function() {
    let appInfo = CC["@mozilla.org/xre/app-info;1"].getService(CI.nsIXULAppInfo);
    let ANDROID_ID = "{aa3c5121-dab2-40e2-81ca-7ea25febc110}";
    return (appInfo.ID === ANDROID_ID);
  },

  chrome_opener: function(uri, args) {
    // we don't use window.open, because we need to work around TorButton's 
    // state control
    args = args || 'chrome,centerscreen';
    return CC['@mozilla.org/appshell/window-mediator;1']
      .getService(CI.nsIWindowMediator) 
      .getMostRecentWindow('navigator:browser')
      .open(uri,'', args );
  },

  tab_opener: function(uri) {
    var gb = CC['@mozilla.org/appshell/window-mediator;1']
      .getService(CI.nsIWindowMediator) 
      .getMostRecentWindow('navigator:browser')
      .gBrowser;
    var tab = gb.addTab(uri);
    gb.selectedTab = tab;
    return tab;
  },

  toggleEnabledState: function() {
    if (this.prefs.getBoolPref("globalEnabled")) {
      try {
        this.obsService.removeObserver(this, "profile-before-change");
        this.obsService.removeObserver(this, "profile-after-change");
        this.obsService.removeObserver(this, "sessionstore-windows-restored");
        OS.removeObserver(this, "cookie-changed");
        OS.removeObserver(this, "http-on-modify-request");
        OS.removeObserver(this, "http-on-examine-merged-response");
        OS.removeObserver(this, "http-on-examine-response");

        var catman = CC["@mozilla.org/categorymanager;1"]
                       .getService(CI.nsICategoryManager);
        catman.deleteCategoryEntry("net-channel-event-sinks",
                                   SERVICE_CTRID, true);

        this.prefs.setBoolPref("globalEnabled", false);
      } catch(e) {
        this.log(WARN, "Couldn't remove observers: " + e);
      }
    } else {
      try {
        this.obsService.addObserver(this, "profile-before-change", false);
        this.obsService.addObserver(this, "profile-after-change", false);
        this.obsService.addObserver(this, "sessionstore-windows-restored", false);
        OS.addObserver(this, "cookie-changed", false);
        OS.addObserver(this, "http-on-modify-request", false);
        OS.addObserver(this, "http-on-examine-merged-response", false);
        OS.addObserver(this, "http-on-examine-response", false);

        var catman = CC["@mozilla.org/categorymanager;1"]
                       .getService(CI.nsICategoryManager);
        // hook on redirections (non persistent, otherwise crashes on 1.8.x)
        catman.addCategoryEntry("net-channel-event-sinks", SERVICE_CTRID,
                                SERVICE_CTRID, false, true);

        HTTPSRules.init();
        this.prefs.setBoolPref("globalEnabled", true);
      } catch(e) {
        this.log(WARN, "Couldn't add observers: " + e);
      }
    }
  },

  toggleHttpNowhere: function() {
    let prefService = Services.prefs;
    let thisBranch =
      prefService.getBranch("extensions.https_everywhere.http_nowhere.");
    let securityBranch = prefService.getBranch("security.");

    // Whether cert is treated as invalid when OCSP connection fails
    let OCSP_REQUIRED = "OCSP.require";

    // Branch to save original settings
    let ORIG_OCSP_REQUIRED = "orig.ocsp.required";


    if (thisBranch.getBoolPref("enabled")) {
      // Restore original OCSP settings. TODO: What if user manually edits
      // these while HTTP Nowhere is enabled?
      let origOcspRequired = thisBranch.getBoolPref(ORIG_OCSP_REQUIRED);
      securityBranch.setBoolPref(OCSP_REQUIRED, origOcspRequired);

      thisBranch.setBoolPref("enabled", false);
      this.httpNowhereEnabled = false;
    } else {
      // Save original OCSP settings in HTTP Nowhere preferences branch.
      let origOcspRequired = securityBranch.getBoolPref(OCSP_REQUIRED);
      thisBranch.setBoolPref(ORIG_OCSP_REQUIRED, origOcspRequired);

      // Disable OCSP enforcement
      securityBranch.setBoolPref(OCSP_REQUIRED, false);

      thisBranch.setBoolPref("enabled", true);
      this.httpNowhereEnabled = true;
    }
  }
};

var prefs = 0;
var econsole = 0;
function https_everywhereLog(level, str) {
  if (prefs == 0) {
    prefs = HTTPSEverywhere.instance.get_prefs();
    econsole = Components.classes["@mozilla.org/consoleservice;1"]
               .getService(Components.interfaces.nsIConsoleService);
  } 
  try {
    var threshold = prefs.getIntPref(LLVAR);
  } catch (e) {
    econsole.logStringMessage( "HTTPS Everywhere: Failed to read about:config LogLevel");
    threshold = WARN;
  }
  if (level >= threshold) {
    var levelName = ["", "VERB", "DBUG", "INFO", "NOTE", "WARN"][level];
    var prefix = "HTTPS Everywhere " + levelName + ": ";
    // dump() prints to browser stdout. That's sometimes undesirable,
    // so only do it when a pref is set (running from test.sh enables
    // this pref).
    if (prefs.getBoolPref("log_to_stdout")) {
      dump(prefix + str + "\n");
    }
    econsole.logStringMessage(prefix + str);
  }
}

/**
* XPCOMUtils.generateNSGetFactory was introduced in Mozilla 2 (Firefox 4).
* XPCOMUtils.generateNSGetModule is for Mozilla 1.9.2 (Firefox 3.6).
*/
if (XPCOMUtils.generateNSGetFactory)
    var NSGetFactory = XPCOMUtils.generateNSGetFactory([HTTPSEverywhere]);
else
    var NSGetModule = XPCOMUtils.generateNSGetModule([HTTPSEverywhere]);

/* vim: set tabstop=4 expandtab: */
            SERVICE_CTRID, false, true);
      }
    } else if (topic == "sessionstore-windows-restored") {