/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

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

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

try {
  // AddonManager.jsm doesn't allow itself to be imported in the child
  // process. We're used in the child process (for now), so guard against
  // this.
  Components.utils.import("resource://gre/modules/AddonManager.jsm");
  /* globals AddonManagerPrivate*/
} catch (e) {
}

XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
                                  "resource://gre/modules/FileUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
                                  "resource://gre/modules/UpdateUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS",
                                  "resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ServiceRequest",
                                  "resource://gre/modules/ServiceRequest.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                  "resource://gre/modules/Task.jsm");

const TOOLKIT_ID                      = "toolkit@mozilla.org";
const KEY_PROFILEDIR                  = "ProfD";
const KEY_APPDIR                      = "XCurProcD";
const FILE_BLOCKLIST                  = "blocklist.xml";
const PREF_BLOCKLIST_LASTUPDATETIME   = "app.update.lastUpdateTime.blocklist-background-update-timer";
const PREF_BLOCKLIST_URL              = "extensions.blocklist.url";
const PREF_BLOCKLIST_ITEM_URL         = "extensions.blocklist.itemURL";
const PREF_BLOCKLIST_ENABLED          = "extensions.blocklist.enabled";
const PREF_BLOCKLIST_INTERVAL         = "extensions.blocklist.interval";
const PREF_BLOCKLIST_LEVEL            = "extensions.blocklist.level";
const PREF_BLOCKLIST_PINGCOUNTTOTAL   = "extensions.blocklist.pingCountTotal";
const PREF_BLOCKLIST_PINGCOUNTVERSION = "extensions.blocklist.pingCountVersion";
const PREF_BLOCKLIST_SUPPRESSUI       = "extensions.blocklist.suppressUI";
const PREF_ONECRL_VIA_AMO             = "security.onecrl.via.amo";
const PREF_BLOCKLIST_UPDATE_ENABLED   = "services.blocklist.update_enabled";
const PREF_GENERAL_USERAGENT_LOCALE   = "general.useragent.locale";
const PREF_APP_DISTRIBUTION           = "distribution.id";
const PREF_APP_DISTRIBUTION_VERSION   = "distribution.version";
const PREF_EM_LOGGING_ENABLED         = "extensions.logging.enabled";
const XMLURI_BLOCKLIST                = "http://www.mozilla.org/2006/addons-blocklist";
const XMLURI_PARSE_ERROR              = "http://www.mozilla.org/newlayout/xml/parsererror.xml"
const UNKNOWN_XPCOM_ABI               = "unknownABI";
const URI_BLOCKLIST_DIALOG            = "chrome://mozapps/content/extensions/blocklist.xul"
const DEFAULT_SEVERITY                = 3;
const DEFAULT_LEVEL                   = 2;
const MAX_BLOCK_LEVEL                 = 3;
const SEVERITY_OUTDATED               = 0;
const VULNERABILITYSTATUS_NONE             = 0;
const VULNERABILITYSTATUS_UPDATE_AVAILABLE = 1;
const VULNERABILITYSTATUS_NO_UPDATE        = 2;

const EXTENSION_BLOCK_FILTERS = ["id", "name", "creator", "homepageURL", "updateURL"];

var gLoggingEnabled = null;
var gBlocklistEnabled = true;
var gBlocklistLevel = DEFAULT_LEVEL;

XPCOMUtils.defineLazyServiceGetter(this, "gConsole",
                                   "@mozilla.org/consoleservice;1",
                                   "nsIConsoleService");

XPCOMUtils.defineLazyServiceGetter(this, "gVersionChecker",
                                   "@mozilla.org/xpcom/version-comparator;1",
                                   "nsIVersionComparator");

XPCOMUtils.defineLazyServiceGetter(this, "gCertBlocklistService",
                                   "@mozilla.org/security/certblocklist;1",
                                   "nsICertBlocklist");

XPCOMUtils.defineLazyGetter(this, "gPref", function() {
  return Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefService).
         QueryInterface(Ci.nsIPrefBranch);
});

// From appinfo in Services.jsm. It is not possible to use the one in
// Services.jsm since it will not successfully QueryInterface nsIXULAppInfo in
// xpcshell tests due to other code calling Services.appinfo before the
// nsIXULAppInfo is created by the tests.
XPCOMUtils.defineLazyGetter(this, "gApp", function() {
  let appinfo = Cc["@mozilla.org/xre/app-info;1"]
                  .getService(Ci.nsIXULRuntime);
  try {
    appinfo.QueryInterface(Ci.nsIXULAppInfo);
  } catch (ex) {
    // Not all applications implement nsIXULAppInfo (e.g. xpcshell doesn't).
    if (!(ex instanceof Components.Exception) ||
        ex.result != Cr.NS_NOINTERFACE)
      throw ex;
  }
  return appinfo;
});

XPCOMUtils.defineLazyGetter(this, "gABI", function() {
  let abi = null;
  try {
    abi = gApp.XPCOMABI;
  }
  catch (e) {
    LOG("BlockList Global gABI: XPCOM ABI unknown.");
  }

  if (AppConstants.platform == "macosx") {
    // Mac universal build should report a different ABI than either macppc
    // or mactel.
    let macutils = Cc["@mozilla.org/xpcom/mac-utils;1"].
                   getService(Ci.nsIMacUtils);

    if (macutils.isUniversalBinary)
      abi += "-u-" + macutils.architecturesInBinary;
  }
  return abi;
});

XPCOMUtils.defineLazyGetter(this, "gOSVersion", function() {
  let osVersion;
  let sysInfo = Cc["@mozilla.org/system-info;1"].
                getService(Ci.nsIPropertyBag2);
  try {
    osVersion = sysInfo.getProperty("name") + " " + sysInfo.getProperty("version");
  }
  catch (e) {
    LOG("BlockList Global gOSVersion: OS Version unknown.");
  }

  if (osVersion) {
    try {
      osVersion += " (" + sysInfo.getProperty("secondaryLibrary") + ")";
    }
    catch (e) {
      // Not all platforms have a secondary widget library, so an error is nothing to worry about.
    }
    osVersion = encodeURIComponent(osVersion);
  }
  return osVersion;
});

// shared code for suppressing bad cert dialogs
XPCOMUtils.defineLazyGetter(this, "gCertUtils", function() {
  let temp = { };
  Components.utils.import("resource://gre/modules/CertUtils.jsm", temp);
  return temp;
});

/**
 * Logs a string to the error console.
 * @param   string
 *          The string to write to the error console..
 */
function LOG(string) {
  if (gLoggingEnabled) {
    dump("*** " + string + "\n");
    gConsole.logStringMessage(string);
  }
}

/**
 * Gets a preference value, handling the case where there is no default.
 * @param   func
 *          The name of the preference function to call, on nsIPrefBranch
 * @param   preference
 *          The name of the preference
 * @param   defaultValue
 *          The default value to return in the event the preference has
 *          no setting
 * @returns The value of the preference, or undefined if there was no
 *          user or default value.
 */
function getPref(func, preference, defaultValue) {
  try {
    return gPref[func](preference);
  }
  catch (e) {
  }
  return defaultValue;
}

/**
 * Constructs a URI to a spec.
 * @param   spec
 *          The spec to construct a URI to
 * @returns The nsIURI constructed.
 */
function newURI(spec) {
  var ioServ = Cc["@mozilla.org/network/io-service;1"].
               getService(Ci.nsIIOService);
  return ioServ.newURI(spec, null, null);
}

// Restarts the application checking in with observers first
function restartApp() {
  // Notify all windows that an application quit has been requested.
  var os = Cc["@mozilla.org/observer-service;1"].
           getService(Ci.nsIObserverService);
  var cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].
                   createInstance(Ci.nsISupportsPRBool);
  os.notifyObservers(cancelQuit, "quit-application-requested", null);

  // Something aborted the quit process.
  if (cancelQuit.data)
    return;

  var as = Cc["@mozilla.org/toolkit/app-startup;1"].
           getService(Ci.nsIAppStartup);
  as.quit(Ci.nsIAppStartup.eRestart | Ci.nsIAppStartup.eAttemptQuit);
}

/**
 * Checks whether this blocklist element is valid for the current OS and ABI.
 * If the element has an "os" attribute then the current OS must appear in
 * its comma separated list for the element to be valid. Similarly for the
 * xpcomabi attribute.
 */
function matchesOSABI(blocklistElement) {
  if (blocklistElement.hasAttribute("os")) {
    var choices = blocklistElement.getAttribute("os").split(",");
    if (choices.length > 0 && choices.indexOf(gApp.OS) < 0)
      return false;
  }

  if (blocklistElement.hasAttribute("xpcomabi")) {
    choices = blocklistElement.getAttribute("xpcomabi").split(",");
    if (choices.length > 0 && choices.indexOf(gApp.XPCOMABI) < 0)
      return false;
  }

  return true;
}

/**
 * Gets the current value of the locale.  It's possible for this preference to
 * be localized, so we have to do a little extra work here.  Similar code
 * exists in nsHttpHandler.cpp when building the UA string.
 */
function getLocale() {
  try {
      // Get the default branch
      var defaultPrefs = gPref.getDefaultBranch(null);
      return defaultPrefs.getComplexValue(PREF_GENERAL_USERAGENT_LOCALE,
                                          Ci.nsIPrefLocalizedString).data;
  } catch (e) {}

  return gPref.getCharPref(PREF_GENERAL_USERAGENT_LOCALE);
}

/* Get the distribution pref values, from defaults only */
function getDistributionPrefValue(aPrefName) {
  var prefValue = "default";

  var defaults = gPref.getDefaultBranch(null);
  try {
    prefValue = defaults.getCharPref(aPrefName);
  } catch (e) {
    // use default when pref not found
  }

  return prefValue;
}

/**
 * Parse a string representation of a regular expression. Needed because we
 * use the /pattern/flags form (because it's detectable), which is only
 * supported as a literal in JS.
 *
 * @param  aStr
 *         String representation of regexp
 * @return RegExp instance
 */
function parseRegExp(aStr) {
  let lastSlash = aStr.lastIndexOf("/");
  let pattern = aStr.slice(1, lastSlash);
  let flags = aStr.slice(lastSlash + 1);
  return new RegExp(pattern, flags);
}

/**
 * Manages the Blocklist. The Blocklist is a representation of the contents of
 * blocklist.xml and allows us to remotely disable / re-enable blocklisted
 * items managed by the Extension Manager with an item's appDisabled property.
 * It also blocklists plugins with data from blocklist.xml.
 */

function Blocklist() {
  Services.obs.addObserver(this, "xpcom-shutdown", false);
  Services.obs.addObserver(this, "sessionstore-windows-restored", false);
  gLoggingEnabled = getPref("getBoolPref", PREF_EM_LOGGING_ENABLED, false);
  gBlocklistEnabled = getPref("getBoolPref", PREF_BLOCKLIST_ENABLED, true);
  gBlocklistLevel = Math.min(getPref("getIntPref", PREF_BLOCKLIST_LEVEL, DEFAULT_LEVEL),
                                     MAX_BLOCK_LEVEL);
  gPref.addObserver("extensions.blocklist.", this, false);
  gPref.addObserver(PREF_EM_LOGGING_ENABLED, this, false);
  this.wrappedJSObject = this;
  // requests from child processes come in here, see receiveMessage.
  Services.ppmm.addMessageListener("Blocklist:getPluginBlocklistState", this);
  Services.ppmm.addMessageListener("Blocklist:content-blocklist-updated", this);
}

Blocklist.prototype = {
  /**
   * Extension ID -> array of Version Ranges
   * Each value in the version range array is a JS Object that has the
   * following properties:
   *   "minVersion"  The minimum version in a version range (default = 0)
   *   "maxVersion"  The maximum version in a version range (default = *)
   *   "targetApps"  Application ID -> array of Version Ranges
   *                 (default = current application ID)
   *                 Each value in the version range array is a JS Object that
   *                 has the following properties:
   *                   "minVersion"  The minimum version in a version range
   *                                 (default = 0)
   *                   "maxVersion"  The maximum version in a version range
   *                                 (default = *)
   */
  _addonEntries: null,
  _gfxEntries: null,
  _pluginEntries: null,

  shutdown: function() {
    Services.obs.removeObserver(this, "xpcom-shutdown");
    Services.ppmm.removeMessageListener("Blocklist:getPluginBlocklistState", this);
    Services.ppmm.removeMessageListener("Blocklist:content-blocklist-updated", this);
    gPref.removeObserver("extensions.blocklist.", this);
    gPref.removeObserver(PREF_EM_LOGGING_ENABLED, this);
  },

  observe: function(aSubject, aTopic, aData) {
    switch (aTopic) {
    case "xpcom-shutdown":
      this.shutdown();
      break;
    case "nsPref:changed":
      switch (aData) {
        case PREF_EM_LOGGING_ENABLED:
          gLoggingEnabled = getPref("getBoolPref", PREF_EM_LOGGING_ENABLED, false);
          break;
        case PREF_BLOCKLIST_ENABLED:
          gBlocklistEnabled = getPref("getBoolPref", PREF_BLOCKLIST_ENABLED, true);
          this._loadBlocklist();
          this._blocklistUpdated(null, null);
          break;
        case PREF_BLOCKLIST_LEVEL:
          gBlocklistLevel = Math.min(getPref("getIntPref", PREF_BLOCKLIST_LEVEL, DEFAULT_LEVEL),
                                     MAX_BLOCK_LEVEL);
          this._blocklistUpdated(null, null);
          break;
      }
      break;
    case "sessionstore-windows-restored":
      Services.obs.removeObserver(this, "sessionstore-windows-restored");
      this._preloadBlocklist();
      break;
    }
  },

  // Message manager message handlers
  receiveMessage: function(aMsg) {
    switch (aMsg.name) {
      case "Blocklist:getPluginBlocklistState":
        return this.getPluginBlocklistState(aMsg.data.addonData,
                                            aMsg.data.appVersion,
                                            aMsg.data.toolkitVersion);
      case "Blocklist:content-blocklist-updated":
        Services.obs.notifyObservers(null, "content-blocklist-updated", null);
        break;
      default:
        throw new Error("Unknown blocklist message received from content: " + aMsg.name);
    }
    return undefined;
  },

  /* See nsIBlocklistService */
  isAddonBlocklisted: function(addon, appVersion, toolkitVersion) {
    return this.getAddonBlocklistState(addon, appVersion, toolkitVersion) ==
                   Ci.nsIBlocklistService.STATE_BLOCKED;
  },

  /* See nsIBlocklistService */
  getAddonBlocklistState: function(addon, appVersion, toolkitVersion) {
    if (!this._isBlocklistLoaded())
      this._loadBlocklist();
    return this._getAddonBlocklistState(addon, this._addonEntries,
                                        appVersion, toolkitVersion);
  },

  /**
   * Private version of getAddonBlocklistState that allows the caller to pass in
   * the add-on blocklist entries to compare against.
   *
   * @param   id
   *          The ID of the item to get the blocklist state for.
   * @param   version
   *          The version of the item to get the blocklist state for.
   * @param   addonEntries
   *          The add-on blocklist entries to compare against.
   * @param   appVersion
   *          The application version to compare to, will use the current
   *          version if null.
   * @param   toolkitVersion
   *          The toolkit version to compare to, will use the current version if
   *          null.
   * @returns The blocklist state for the item, one of the STATE constants as
   *          defined in nsIBlocklistService.
   */
  _getAddonBlocklistState: function(addon, addonEntries, appVersion, toolkitVersion) {
    if (!gBlocklistEnabled)
      return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;

    // Not all applications implement nsIXULAppInfo (e.g. xpcshell doesn't).
    if (!appVersion && !gApp.version)
      return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;

    if (!appVersion)
      appVersion = gApp.version;
    if (!toolkitVersion)
      toolkitVersion = gApp.platformVersion;

    var blItem = this._findMatchingAddonEntry(addonEntries, addon);
    if (!blItem)
      return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;

    for (let currentblItem of blItem.versions) {
      if (currentblItem.includesItem(addon.version, appVersion, toolkitVersion))
        return currentblItem.severity >= gBlocklistLevel ? Ci.nsIBlocklistService.STATE_BLOCKED :
                                                       Ci.nsIBlocklistService.STATE_SOFTBLOCKED;
    }
    return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
  },

  /**
   * Returns the set of prefs of the add-on stored in the blocklist file
   * (probably to revert them on disabling).
   * @param addon
   *        The add-on whose to-be-reset prefs are to be found.
   */
  _getAddonPrefs: function(addon) {
    let entry = this._findMatchingAddonEntry(this._addonEntries, addon);
    return entry.prefs.slice(0);
  },

  _findMatchingAddonEntry: function(aAddonEntries, aAddon) {
    if (!aAddon)
      return null;
    // Returns true if the params object passes the constraints set by entry.
    // (For every non-null property in entry, the same key must exist in
    // params and value must be the same)
    function checkEntry(entry, params) {
      for (let [key, value] of entry) {
        if (value === null || value === undefined)
          continue;
        if (params[key]) {
          if (value instanceof RegExp) {
            if (!value.test(params[key])) {
              return false;
            }
          } else if (value !== params[key]) {
            return false;
          }
        } else {
          return false;
        }
      }
      return true;
    }

    let params = {};
    for (let filter of EXTENSION_BLOCK_FILTERS) {
      params[filter] = aAddon[filter];
    }
    if (params.creator)
      params.creator = params.creator.name;
    for (let entry of aAddonEntries) {
      if (checkEntry(entry.attributes, params)) {
         return entry;
       }
     }
     return null;
  },

  /* See nsIBlocklistService */
  getAddonBlocklistURL: function(addon, appVersion, toolkitVersion) {
    if (!gBlocklistEnabled)
      return "";

    if (!this._isBlocklistLoaded())
      this._loadBlocklist();

    let blItem = this._findMatchingAddonEntry(this._addonEntries, addon);
    if (!blItem || !blItem.blockID)
      return null;

    return this._createBlocklistURL(blItem.blockID);
  },

  _createBlocklistURL: function(id) {
    let url = Services.urlFormatter.formatURLPref(PREF_BLOCKLIST_ITEM_URL);
    url = url.replace(/%blockID%/g, id);

    return url;
  },

  notify: function(aTimer) {
    if (!gBlocklistEnabled)
      return;

    try {
      var dsURI = gPref.getCharPref(PREF_BLOCKLIST_URL);
    }
    catch (e) {
      LOG("Blocklist::notify: The " + PREF_BLOCKLIST_URL + " preference" +
          " is missing!");
      return;
    }

    var pingCountVersion = getPref("getIntPref", PREF_BLOCKLIST_PINGCOUNTVERSION, 0);
    var pingCountTotal = getPref("getIntPref", PREF_BLOCKLIST_PINGCOUNTTOTAL, 1);
    var daysSinceLastPing = 0;
    if (pingCountVersion == 0) {
      daysSinceLastPing = "new";
    }
    else {
      // Seconds in one day is used because nsIUpdateTimerManager stores the
      // last update time in seconds.
      let secondsInDay = 60 * 60 * 24;
      let lastUpdateTime = getPref("getIntPref", PREF_BLOCKLIST_LASTUPDATETIME, 0);
      if (lastUpdateTime == 0) {
        daysSinceLastPing = "invalid";
      }
      else {
        let now = Math.round(Date.now() / 1000);
        daysSinceLastPing = Math.floor((now - lastUpdateTime) / secondsInDay);
      }

      if (daysSinceLastPing == 0 || daysSinceLastPing == "invalid") {
        pingCountVersion = pingCountTotal = "invalid";
      }
    }

    if (pingCountVersion < 1)
      pingCountVersion = 1;
    if (pingCountTotal < 1)
      pingCountTotal = 1;

    dsURI = dsURI.replace(/%APP_ID%/g, gApp.ID);
    // Not all applications implement nsIXULAppInfo (e.g. xpcshell doesn't).
    if (gApp.version)
      dsURI = dsURI.replace(/%APP_VERSION%/g, gApp.version);
    dsURI = dsURI.replace(/%PRODUCT%/g, gApp.name);
    // Not all applications implement nsIXULAppInfo (e.g. xpcshell doesn't).
    if (gApp.version)
      dsURI = dsURI.replace(/%VERSION%/g, gApp.version);
    dsURI = dsURI.replace(/%BUILD_ID%/g, gApp.appBuildID);
    dsURI = dsURI.replace(/%BUILD_TARGET%/g, gApp.OS + "_" + gABI);
    dsURI = dsURI.replace(/%OS_VERSION%/g, gOSVersion);
    dsURI = dsURI.replace(/%LOCALE%/g, getLocale());
    dsURI = dsURI.replace(/%CHANNEL%/g, UpdateUtils.UpdateChannel);
    dsURI = dsURI.replace(/%PLATFORM_VERSION%/g, gApp.platformVersion);
    dsURI = dsURI.replace(/%DISTRIBUTION%/g,
                      getDistributionPrefValue(PREF_APP_DISTRIBUTION));
    dsURI = dsURI.replace(/%DISTRIBUTION_VERSION%/g,
                      getDistributionPrefValue(PREF_APP_DISTRIBUTION_VERSION));
    dsURI = dsURI.replace(/%PING_COUNT%/g, pingCountVersion);
    dsURI = dsURI.replace(/%TOTAL_PING_COUNT%/g, pingCountTotal);
    dsURI = dsURI.replace(/%DAYS_SINCE_LAST_PING%/g, daysSinceLastPing);
    dsURI = dsURI.replace(/\+/g, "%2B");

    // Under normal operations it will take around 5,883,516 years before the
    // preferences used to store pingCountVersion and pingCountTotal will rollover
    // so this code doesn't bother trying to do the "right thing" here.
    if (pingCountVersion != "invalid") {
      pingCountVersion++;
      if (pingCountVersion > 2147483647) {
        // Rollover to -1 if the value is greater than what is support by an
        // integer preference. The -1 indicates that the counter has been reset.
        pingCountVersion = -1;
      }
      gPref.setIntPref(PREF_BLOCKLIST_PINGCOUNTVERSION, pingCountVersion);
    }

    if (pingCountTotal != "invalid") {
      pingCountTotal++;
      if (pingCountTotal > 2147483647) {
        // Rollover to 1 if the value is greater than what is support by an
        // integer preference.
        pingCountTotal = -1;
      }
      gPref.setIntPref(PREF_BLOCKLIST_PINGCOUNTTOTAL, pingCountTotal);
    }

    // Verify that the URI is valid
    try {
      var uri = newURI(dsURI);
    }
    catch (e) {
      LOG("Blocklist::notify: There was an error creating the blocklist URI\r\n" +
          "for: " + dsURI + ", error: " + e);
      return;
    }

    LOG("Blocklist::notify: Requesting " + uri.spec);
    let request = new ServiceRequest();
    request.open("GET", uri.spec, true);
    request.channel.notificationCallbacks = new gCertUtils.BadCertHandler();
    request.overrideMimeType("text/xml");
    request.setRequestHeader("Cache-Control", "no-cache");
    request.QueryInterface(Components.interfaces.nsIJSXMLHttpRequest);

    request.addEventListener("error", event => this.onXMLError(event), false);
    request.addEventListener("load", event => this.onXMLLoad(event), false);
    request.send(null);

    // When the blocklist loads we need to compare it to the current copy so
    // make sure we have loaded it.
    if (!this._isBlocklistLoaded())
      this._loadBlocklist();

    // If kinto update is enabled, do the kinto update
    if (gPref.getBoolPref(PREF_BLOCKLIST_UPDATE_ENABLED)) {
      const updater =
        Components.utils.import("resource://services-common/blocklist-updater.js",
                                {});
      updater.checkVersions().catch(() => {
        // Before we enable this in release, we want to collect telemetry on
        // failed kinto updates - see bug 1254099
      });
    }
  },

  onXMLLoad: Task.async(function*(aEvent) {
    let request = aEvent.target;
    try {
      gCertUtils.checkCert(request.channel);
    }
    catch (e) {
      LOG("Blocklist::onXMLLoad: " + e);
      return;
    }
    let responseXML = request.responseXML;
    if (!responseXML || responseXML.documentElement.namespaceURI == XMLURI_PARSE_ERROR ||
        (request.status != 200 && request.status != 0)) {
      LOG("Blocklist::onXMLLoad: there was an error during load");
      return;
    }

    var oldAddonEntries = this._addonEntries;
    var oldPluginEntries = this._pluginEntries;
    this._addonEntries = [];
    this._gfxEntries = [];
    this._pluginEntries = [];

    this._loadBlocklistFromString(request.responseText);
    // We don't inform the users when the graphics blocklist changed at runtime.
    // However addons and plugins blocking status is refreshed.
    this._blocklistUpdated(oldAddonEntries, oldPluginEntries);

    try {
      let path = OS.Path.join(OS.Constants.Path.profileDir, FILE_BLOCKLIST);
      yield OS.File.writeAtomic(path, request.responseText, {tmpPath: path + ".tmp"});
    } catch (e) {
      LOG("Blocklist::onXMLLoad: " + e);
    }
  }),

  onXMLError: function(aEvent) {
    try {
      var request = aEvent.target;
      // the following may throw (e.g. a local file or timeout)
      var status = request.status;
    }
    catch (e) {
      request = aEvent.target.channel.QueryInterface(Ci.nsIRequest);
      status = request.status;
    }
    var statusText = "nsIXMLHttpRequest channel unavailable";
    // When status is 0 we don't have a valid channel.
    if (status != 0) {
      try {
        statusText = request.statusText;
      } catch (e) {
      }
    }
    LOG("Blocklist:onError: There was an error loading the blocklist file\r\n" +
        statusText);
  },

  /**
   * Finds the newest blocklist file from the application and the profile and
   * load it or does nothing if neither exist.
   */
  _loadBlocklist: function() {
    this._addonEntries = [];
    this._gfxEntries = [];
    this._pluginEntries = [];
    var profFile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_BLOCKLIST]);
    if (profFile.exists()) {
      this._loadBlocklistFromFile(profFile);
      return;
    }
    var appFile = FileUtils.getFile(KEY_APPDIR, [FILE_BLOCKLIST]);
    if (appFile.exists()) {
      this._loadBlocklistFromFile(appFile);
      return;
    }
    LOG("Blocklist::_loadBlocklist: no XML File found");
  },

  /**
#    The blocklist XML file looks something like this:
#
#    <blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist">
#      <emItems>
#        <emItem id="item_1@domain" blockID="i1">
#          <prefs>
#            <pref>accessibility.accesskeycausesactivation</pref>
#            <pref>accessibility.blockautorefresh</pref>
#          </prefs>
#          <versionRange minVersion="1.0" maxVersion="2.0.*">
#            <targetApplication id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}">
#              <versionRange minVersion="1.5" maxVersion="1.5.*"/>
#              <versionRange minVersion="1.7" maxVersion="1.7.*"/>
#            </targetApplication>
#            <targetApplication id="toolkit@mozilla.org">
#              <versionRange minVersion="1.9" maxVersion="1.9.*"/>
#            </targetApplication>
#          </versionRange>
#          <versionRange minVersion="3.0" maxVersion="3.0.*">
#            <targetApplication id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}">
#              <versionRange minVersion="1.5" maxVersion="1.5.*"/>
#            </targetApplication>
#            <targetApplication id="toolkit@mozilla.org">
#              <versionRange minVersion="1.9" maxVersion="1.9.*"/>
#            </targetApplication>
#          </versionRange>
#        </emItem>
#        <emItem id="item_2@domain" blockID="i2">
#          <versionRange minVersion="3.1" maxVersion="4.*"/>
#        </emItem>
#        <emItem id="item_3@domain">
#          <versionRange>
#            <targetApplication id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}">
#              <versionRange minVersion="1.5" maxVersion="1.5.*"/>
#            </targetApplication>
#          </versionRange>
#        </emItem>
#        <emItem id="item_4@domain" blockID="i3">
#          <versionRange>
#            <targetApplication>
#              <versionRange minVersion="1.5" maxVersion="1.5.*"/>
#            </targetApplication>
#          </versionRange>
#        <emItem id="/@badperson\.com$/"/>
#      </emItems>
#      <pluginItems>
#        <pluginItem blockID="i4">
#          <!-- All match tags must match a plugin to blocklist a plugin -->
#          <match name="name" exp="some plugin"/>
#          <match name="description" exp="1[.]2[.]3"/>
#        </pluginItem>
#      </pluginItems>
#      <certItems>
#        <!-- issuerName is the DER issuer name data base64 encoded... -->
#        <certItem issuerName="MA0xCzAJBgNVBAMMAmNh">
#          <!-- ... as is the serial number DER data -->
#          <serialNumber>AkHVNA==</serialNumber>
#        </certItem>
#        <!-- subject is the DER subject name data base64 encoded... -->
#        <certItem subject="MA0xCzAJBgNVBAMMAmNh" pubKeyHash="/xeHA5s+i9/z9d8qy6JEuE1xGoRYIwgJuTE/lmaGJ7M=">
#        </certItem>
#      </certItems>
#    </blocklist>
   */

  _loadBlocklistFromFile: function(file) {
    if (!gBlocklistEnabled) {
      LOG("Blocklist::_loadBlocklistFromFile: blocklist is disabled");
      return;
    }

    let telemetry = Services.telemetry;

    if (this._isBlocklistPreloaded()) {
      telemetry.getHistogramById("BLOCKLIST_SYNC_FILE_LOAD").add(false);
      this._loadBlocklistFromString(this._preloadedBlocklistContent);
      delete this._preloadedBlocklistContent;
      return;
    }

    if (!file.exists()) {
      LOG("Blocklist::_loadBlocklistFromFile: XML File does not exist " + file.path);
      return;
    }

    telemetry.getHistogramById("BLOCKLIST_SYNC_FILE_LOAD").add(true);

    let text = "";
    let fstream = null;
    let cstream = null;

    try {
      fstream = Components.classes["@mozilla.org/network/file-input-stream;1"]
                          .createInstance(Components.interfaces.nsIFileInputStream);
      cstream = Components.classes["@mozilla.org/intl/converter-input-stream;1"]
                          .createInstance(Components.interfaces.nsIConverterInputStream);

      fstream.init(file, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0);
      cstream.init(fstream, "UTF-8", 0, 0);

      let str = {};
      let read = 0;

      do {
        read = cstream.readString(0xffffffff, str); // read as much as we can and put it in str.value
        text += str.value;
      } while (read != 0);
    } catch (e) {
      LOG("Blocklist::_loadBlocklistFromFile: Failed to load XML file " + e);
    } finally {
      if (cstream)
        cstream.close();
      if (fstream)
        fstream.close();
    }

    if (text)
        this._loadBlocklistFromString(text);
  },

  _isBlocklistLoaded: function() {
    return this._addonEntries != null && this._gfxEntries != null && this._pluginEntries != null;
  },

  _isBlocklistPreloaded: function() {
    return this._preloadedBlocklistContent != null;
  },

  /* Used for testing */
  _clear: function() {
    this._addonEntries = null;
    this._gfxEntries = null;
    this._pluginEntries = null;
    this._preloadedBlocklistContent = null;
  },

  _preloadBlocklist: Task.async(function*() {
    let profPath = OS.Path.join(OS.Constants.Path.profileDir, FILE_BLOCKLIST);
    try {
      yield this._preloadBlocklistFile(profPath);
      return;
    } catch (e) {
      LOG("Blocklist::_preloadBlocklist: Failed to load XML file " + e)
    }

    var appFile = FileUtils.getFile(KEY_APPDIR, [FILE_BLOCKLIST]);
    try {
      yield this._preloadBlocklistFile(appFile.path);
      return;
    } catch (e) {
      LOG("Blocklist::_preloadBlocklist: Failed to load XML file " + e)
    }

    LOG("Blocklist::_preloadBlocklist: no XML File found");
  }),

  _preloadBlocklistFile: Task.async(function*(path) {
    if (this._addonEntries) {
      // The file has been already loaded.
      return;
    }

    if (!gBlocklistEnabled) {
      LOG("Blocklist::_preloadBlocklistFile: blocklist is disabled");
      return;
    }

    let text = yield OS.File.read(path, { encoding: "utf-8" });

    if (!this._addonEntries) {
      // Store the content only if a sync load has not been performed in the meantime.
      this._preloadedBlocklistContent = text;
    }
  }),

  _loadBlocklistFromString : function(text) {
    try {
      var parser = Cc["@mozilla.org/xmlextras/domparser;1"].
                   createInstance(Ci.nsIDOMParser);
      var doc = parser.parseFromString(text, "text/xml");
      if (doc.documentElement.namespaceURI != XMLURI_BLOCKLIST) {
        LOG("Blocklist::_loadBlocklistFromFile: aborting due to incorrect " +
            "XML Namespace.\r\nExpected: " + XMLURI_BLOCKLIST + "\r\n" +
            "Received: " + doc.documentElement.namespaceURI);
        return;
      }

      var populateCertBlocklist = getPref("getBoolPref", PREF_ONECRL_VIA_AMO, true);

      var childNodes = doc.documentElement.childNodes;
      for (let element of childNodes) {
        if (!(element instanceof Ci.nsIDOMElement))
          continue;
        switch (element.localName) {
        case "emItems":
          this._addonEntries = this._processItemNodes(element.childNodes, "emItem",
                                                      this._handleEmItemNode);
          break;
        case "pluginItems":
          // We don't support plugins on b2g.
          if (AppConstants.MOZ_B2G) {
            return;
          }
          this._pluginEntries = this._processItemNodes(element.childNodes, "pluginItem",
                                                       this._handlePluginItemNode);
          break;
        case "certItems":
          if (populateCertBlocklist) {
            this._processItemNodes(element.childNodes, "certItem",
                                   this._handleCertItemNode.bind(this));
          }
          break;
        case "gfxItems":
          // Parse as simple list of objects.
          this._gfxEntries = this._processItemNodes(element.childNodes, "gfxBlacklistEntry",
                                                    this._handleGfxBlacklistNode);
          break;
        default:
          LOG("Blocklist::_loadBlocklistFromString: ignored entries " + element.localName);
        }
      }
      if (populateCertBlocklist) {
        gCertBlocklistService.saveEntries();
      }
      if (this._gfxEntries.length > 0) {
        this._notifyObserversBlocklistGFX();
      }
    }
    catch (e) {
      LOG("Blocklist::_loadBlocklistFromFile: Error constructing blocklist " + e);
      return;
    }
  },

  _processItemNodes: function(itemNodes, itemName, handler) {
    var result = [];
    for (var i = 0; i < itemNodes.length; ++i) {
      var blocklistElement = itemNodes.item(i);
      if (!(blocklistElement instanceof Ci.nsIDOMElement) ||
          blocklistElement.localName != itemName)
        continue;

      handler(blocklistElement, result);
    }
    return result;
  },

  _handleCertItemNode: function(blocklistElement, result) {
    let issuer = blocklistElement.getAttribute("issuerName");
    if (issuer) {
      for (let snElement of blocklistElement.children) {
        try {
          gCertBlocklistService.revokeCertByIssuerAndSerial(issuer, snElement.textContent);
        } catch (e) {
          // we want to keep trying other elements since missing all items
          // is worse than missing one
          LOG("Blocklist::_handleCertItemNode: Error adding revoked cert by Issuer and Serial" + e);
        }
      }
      return;
    }

    let pubKeyHash = blocklistElement.getAttribute("pubKeyHash");
    let subject = blocklistElement.getAttribute("subject");

    if (pubKeyHash && subject) {
      try {
        gCertBlocklistService.revokeCertBySubjectAndPubKey(subject, pubKeyHash);
      } catch (e) {
        LOG("Blocklist::_handleCertItemNode: Error adding revoked cert by Subject and PubKey" + e);
      }
    }
  },

  _handleEmItemNode: function(blocklistElement, result) {
    if (!matchesOSABI(blocklistElement))
      return;

    let blockEntry = {
      versions: [],
      prefs: [],
      blockID: null,
      attributes: new Map()
      // Atleast one of EXTENSION_BLOCK_FILTERS must get added to attributes
    };

    // Any filter starting with '/' is interpreted as a regex. So if an attribute
    // starts with a '/' it must be checked via a regex.
    function regExpCheck(attr) {
      return attr.startsWith("/") ? parseRegExp(attr) : attr;
    }

    for (let filter of EXTENSION_BLOCK_FILTERS) {
      let attr = blocklistElement.getAttribute(filter);
      if (attr)
        blockEntry.attributes.set(filter, regExpCheck(attr));
    }

    var childNodes = blocklistElement.childNodes;

    for (let x = 0; x < childNodes.length; x++) {
      var childElement = childNodes.item(x);
      if (!(childElement instanceof Ci.nsIDOMElement))
        continue;
      if (childElement.localName === "prefs") {
        let prefElements = childElement.childNodes;
        for (let i = 0; i < prefElements.length; i++) {
          let prefElement = prefElements.item(i);
          if (!(prefElement instanceof Ci.nsIDOMElement) ||
              prefElement.localName !== "pref")
            continue;
          blockEntry.prefs.push(prefElement.textContent);
        }
      }
      else if (childElement.localName === "versionRange")
        blockEntry.versions.push(new BlocklistItemData(childElement));
    }
    // if only the extension ID is specified block all versions of the
    // extension for the current application.
    if (blockEntry.versions.length == 0)
      blockEntry.versions.push(new BlocklistItemData(null));

    blockEntry.blockID = blocklistElement.getAttribute("blockID");

    result.push(blockEntry);
  },

  _handlePluginItemNode: function(blocklistElement, result) {
    if (!matchesOSABI(blocklistElement))
      return;

    var matchNodes = blocklistElement.childNodes;
    var blockEntry = {
      matches: {},
      versions: [],
      blockID: null,
      infoURL: null,
    };
    var hasMatch = false;
    for (var x = 0; x < matchNodes.length; ++x) {
      var matchElement = matchNodes.item(x);
      if (!(matchElement instanceof Ci.nsIDOMElement))
        continue;
      if (matchElement.localName == "match") {
        var name = matchElement.getAttribute("name");
        var exp = matchElement.getAttribute("exp");
        try {
          blockEntry.matches[name] = new RegExp(exp, "m");
          hasMatch = true;
        } catch (e) {
          // Ignore invalid regular expressions
        }
      }
      if (matchElement.localName == "versionRange") {
        blockEntry.versions.push(new BlocklistItemData(matchElement));
      }
      else if (matchElement.localName == "infoURL") {
        blockEntry.infoURL = matchElement.textContent;
      }
    }
    // Plugin entries require *something* to match to an actual plugin
    if (!hasMatch)
      return;
    // Add a default versionRange if there wasn't one specified
    if (blockEntry.versions.length == 0)
      blockEntry.versions.push(new BlocklistItemData(null));

    blockEntry.blockID = blocklistElement.getAttribute("blockID");

    result.push(blockEntry);
  },

  // <gfxBlacklistEntry blockID="g60">
  //   <os>WINNT 6.0</os>
  //   <osversion>14</osversion> currently only used for Android
  //   <versionRange minVersion="42.0" maxVersion="13.0b2"/>
  //   <vendor>0x8086</vendor>
  //   <devices>
  //     <device>0x2582</device>
  //     <device>0x2782</device>
  //   </devices>
  //   <feature> DIRECT3D_10_LAYERS </feature>
  //   <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
  //   <driverVersion> 8.52.322.2202 </driverVersion>
  //   <driverVersionMax> 8.52.322.2202 </driverVersionMax>
  //   <driverVersionComparator> LESS_THAN_OR_EQUAL </driverVersionComparator>
  //   <model>foo</model>
  //   <product>foo</product>
  //   <manufacturer>foo</manufacturer>
  //   <hardware>foo</hardware>
  // </gfxBlacklistEntry>
  _handleGfxBlacklistNode: function (blocklistElement, result) {
    const blockEntry = {};

    // The blockID attribute is always present in the actual data produced on server
    // (see https://github.com/mozilla/addons-server/blob/2016.05.05/src/olympia/blocklist/templates/blocklist/blocklist.xml#L74)
    // But it is sometimes missing in test fixtures.
    if (blocklistElement.hasAttribute("blockID")) {
      blockEntry.blockID = blocklistElement.getAttribute("blockID");
    }

    // Trim helper (spaces, tabs, no-break spaces..)
    const trim = (s) => (s || '').replace(/(^[\s\uFEFF\xA0]+)|([\s\uFEFF\xA0]+$)/g, "");

    for (let i = 0; i < blocklistElement.childNodes.length; ++i) {
      var matchElement = blocklistElement.childNodes.item(i);
      if (!(matchElement instanceof Ci.nsIDOMElement))
        continue;

      let value;
      if (matchElement.localName == "devices") {
        value = [];
        for (let j = 0; j < matchElement.childNodes.length; j++) {
          const childElement = matchElement.childNodes.item(j);
          const childValue = trim(childElement.textContent);
          // Make sure no empty value is added.
          if (childValue) {
            if (/,/.test(childValue)) {
              // Devices can't contain comma.
              // (c.f serialization in _notifyObserversBlocklistGFX)
              const e = new Error(`Unsupported device name ${childValue}`);
              Components.utils.reportError(e);
            }
            else {
              value.push(childValue);
            }
          }
        }
      } else if (matchElement.localName == "versionRange") {
        value = {minVersion: trim(matchElement.getAttribute("minVersion")) || "0",
                 maxVersion: trim(matchElement.getAttribute("maxVersion")) || "*"};
      } else {
        value = trim(matchElement.textContent);
      }
      if (value) {
        blockEntry[matchElement.localName] = value;
      }
    }
    result.push(blockEntry);
  },

  /* See nsIBlocklistService */
  getPluginBlocklistState: function(plugin, appVersion, toolkitVersion) {
    if (AppConstants.platform == "android" ||
        AppConstants.MOZ_B2G) {
      return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
    }
    if (!this._isBlocklistLoaded())
      this._loadBlocklist();
    return this._getPluginBlocklistState(plugin, this._pluginEntries,
                                         appVersion, toolkitVersion);
  },

  /**
   * Private helper to get the blocklist entry for a plugin given a set of
   * blocklist entries and versions.
   *
   * @param   plugin
   *          The nsIPluginTag to get the blocklist state for.
   * @param   pluginEntries
   *          The plugin blocklist entries to compare against.
   * @param   appVersion
   *          The application version to compare to, will use the current
   *          version if null.
   * @param   toolkitVersion
   *          The toolkit version to compare to, will use the current version if
   *          null.
   * @returns {entry: blocklistEntry, version: blocklistEntryVersion},
   *          or null if there is no matching entry.
   */
  _getPluginBlocklistEntry: function(plugin, pluginEntries, appVersion, toolkitVersion) {
    if (!gBlocklistEnabled)
      return null;

    // Not all applications implement nsIXULAppInfo (e.g. xpcshell doesn't).
    if (!appVersion && !gApp.version)
      return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;

    if (!appVersion)
      appVersion = gApp.version;
    if (!toolkitVersion)
      toolkitVersion = gApp.platformVersion;

    for (var blockEntry of pluginEntries) {
      var matchFailed = false;
      for (var name in blockEntry.matches) {
        if (!(name in plugin) ||
            typeof(plugin[name]) != "string" ||
            !blockEntry.matches[name].test(plugin[name])) {
          matchFailed = true;
          break;
        }
      }

      if (matchFailed)
        continue;

      for (let blockEntryVersion of blockEntry.versions) {
        if (blockEntryVersion.includesItem(plugin.version, appVersion,
                                           toolkitVersion)) {
          return {entry: blockEntry, version: blockEntryVersion};
        }
      }
    }

    return null;
  },

  /**
   * Private version of getPluginBlocklistState that allows the caller to pass in
   * the plugin blocklist entries.
   *
   * @param   plugin
   *          The nsIPluginTag to get the blocklist state for.
   * @param   pluginEntries
   *          The plugin blocklist entries to compare against.
   * @param   appVersion
   *          The application version to compare to, will use the current
   *          version if null.
   * @param   toolkitVersion
   *          The toolkit version to compare to, will use the current version if
   *          null.
   * @returns The blocklist state for the item, one of the STATE constants as
   *          defined in nsIBlocklistService.
   */
  _getPluginBlocklistState: function(plugin, pluginEntries, appVersion, toolkitVersion) {

    let r = this._getPluginBlocklistEntry(plugin, pluginEntries,
                                          appVersion, toolkitVersion);
    if (!r) {
      return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
    }

    let {entry: blockEntry, version: blockEntryVersion} = r;

    if (blockEntryVersion.severity >= gBlocklistLevel)
      return Ci.nsIBlocklistService.STATE_BLOCKED;
    if (blockEntryVersion.severity == SEVERITY_OUTDATED) {
      let vulnerabilityStatus = blockEntryVersion.vulnerabilityStatus;
      if (vulnerabilityStatus == VULNERABILITYSTATUS_UPDATE_AVAILABLE)
        return Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE;
      if (vulnerabilityStatus == VULNERABILITYSTATUS_NO_UPDATE)
        return Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE;
      return Ci.nsIBlocklistService.STATE_OUTDATED;
    }
    return Ci.nsIBlocklistService.STATE_SOFTBLOCKED;
  },

  /* See nsIBlocklistService */
  getPluginBlocklistURL: function(plugin) {
    if (!this._isBlocklistLoaded())
      this._loadBlocklist();

    let r = this._getPluginBlocklistEntry(plugin, this._pluginEntries);
    if (!r) {
      return null;
    }
    let {entry: blockEntry, version: blockEntryVersion} = r;
    if (!blockEntry.blockID) {
      return null;
    }

    return this._createBlocklistURL(blockEntry.blockID);
  },

  /* See nsIBlocklistService */
  getPluginInfoURL: function(plugin) {
    if (!this._isBlocklistLoaded())
      this._loadBlocklist();

    let r = this._getPluginBlocklistEntry(plugin, this._pluginEntries);
    if (!r) {
      return null;
    }
    let {entry: blockEntry, version: blockEntryVersion} = r;
    if (!blockEntry.blockID) {
      return null;
    }

    return blockEntry.infoURL;
  },

  _notifyObserversBlocklistGFX: function () {
    // Notify `GfxInfoBase`, by passing a string serialization.
    // This way we avoid spreading XML structure logics there.
    const payload = this._gfxEntries.map((r) => {
      return Object.keys(r).sort().filter((k) => !/id|last_modified/.test(k)).map((key) => {
        let value = r[key];
        if (Array.isArray(value)) {
          value = value.join(",");
        } else if (value.hasOwnProperty("minVersion")) {
          // When XML is parsed, both minVersion and maxVersion are set.
          value = `${value.minVersion},${value.maxVersion}`;
        }
        return `${key}:${value}`;
      }).join("\t");
    }).join("\n");
    Services.obs.notifyObservers(null, "blocklist-data-gfxItems", payload);
  },

  _notifyObserversBlocklistUpdated: function() {
    Services.obs.notifyObservers(this, "blocklist-updated", "");
    Services.ppmm.broadcastAsyncMessage("Blocklist:blocklistInvalidated", {});
  },

  _blocklistUpdated: function(oldAddonEntries, oldPluginEntries) {
    if (AppConstants.MOZ_B2G) {
      return;
    }

    var addonList = [];

    // A helper function that reverts the prefs passed to default values.
    function resetPrefs(prefs) {
      for (let pref of prefs)
        gPref.clearUserPref(pref);
    }
    const types = ["extension", "theme", "locale", "dictionary", "service"];
    AddonManager.getAddonsByTypes(types, addons => {
      for (let addon of addons) {
        let oldState = Ci.nsIBlocklistService.STATE_NOTBLOCKED;
        if (oldAddonEntries)
          oldState = this._getAddonBlocklistState(addon, oldAddonEntries);
        let state = this.getAddonBlocklistState(addon);

        LOG("Blocklist state for " + addon.id + " changed from " +
            oldState + " to " + state);

        // We don't want to re-warn about add-ons
        if (state == oldState)
          continue;

        if (state === Ci.nsIBlocklistService.STATE_BLOCKED) {
          // It's a hard block. We must reset certain preferences.
          let prefs = this._getAddonPrefs(addon);
          resetPrefs(prefs);
         }

        // Ensure that softDisabled is false if the add-on is not soft blocked
        if (state != Ci.nsIBlocklistService.STATE_SOFTBLOCKED)
          addon.softDisabled = false;

        // Don't warn about add-ons becoming unblocked.
        if (state == Ci.nsIBlocklistService.STATE_NOT_BLOCKED)
          continue;

        // If an add-on has dropped from hard to soft blocked just mark it as
        // soft disabled and don't warn about it.
        if (state == Ci.nsIBlocklistService.STATE_SOFTBLOCKED &&
            oldState == Ci.nsIBlocklistService.STATE_BLOCKED) {
          addon.softDisabled = true;
          continue;
        }

        // If the add-on is already disabled for some reason then don't warn
        // about it
        if (!addon.isActive) {
          // But mark it as softblocked if necessary. Note that we avoid setting
          // softDisabled at the same time as userDisabled to make it clear
          // which was the original cause of the add-on becoming disabled in a
          // way that the user can change.
          if (state == Ci.nsIBlocklistService.STATE_SOFTBLOCKED && !addon.userDisabled)
            addon.softDisabled = true;
          continue;
        }

        addonList.push({
          name: addon.name,
          version: addon.version,
          icon: addon.iconURL,
          disable: false,
          blocked: state == Ci.nsIBlocklistService.STATE_BLOCKED,
          item: addon,
          url: this.getAddonBlocklistURL(addon),
        });
      }

      AddonManagerPrivate.updateAddonAppDisabledStates();

      var phs = Cc["@mozilla.org/plugin/host;1"].
                getService(Ci.nsIPluginHost);
      var plugins = phs.getPluginTags();

      for (let plugin of plugins) {
        let oldState = -1;
        if (oldPluginEntries)
          oldState = this._getPluginBlocklistState(plugin, oldPluginEntries);
        let state = this.getPluginBlocklistState(plugin);
        LOG("Blocklist state for " + plugin.name + " changed from " +
            oldState + " to " + state);
        // We don't want to re-warn about items
        if (state == oldState)
          continue;

        if (oldState == Ci.nsIBlocklistService.STATE_BLOCKED) {
          if (state == Ci.nsIBlocklistService.STATE_SOFTBLOCKED)
            plugin.enabledState = Ci.nsIPluginTag.STATE_DISABLED;
        }
        else if (!plugin.disabled && state != Ci.nsIBlocklistService.STATE_NOT_BLOCKED) {
          if (state != Ci.nsIBlocklistService.STATE_OUTDATED &&
              state != Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE &&
              state != Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE) {
            addonList.push({
              name: plugin.name,
              version: plugin.version,
              icon: "chrome://mozapps/skin/plugins/pluginGeneric.png",
              disable: false,
              blocked: state == Ci.nsIBlocklistService.STATE_BLOCKED,
              item: plugin,
              url: this.getPluginBlocklistURL(plugin),
            });
          }
        }
      }

      if (addonList.length == 0) {
        this._notifyObserversBlocklistUpdated();
        return;
      }

      if ("@mozilla.org/addons/blocklist-prompt;1" in Cc) {
        try {
          let blockedPrompter = Cc["@mozilla.org/addons/blocklist-prompt;1"]
                                 .getService(Ci.nsIBlocklistPrompt);
          blockedPrompter.prompt(addonList);
        } catch (e) {
          LOG(e);
        }
        this._notifyObserversBlocklistUpdated();
        return;
      }

      var args = {
        restart: false,
        list: addonList
      };
      // This lets the dialog get the raw js object
      args.wrappedJSObject = args;

      /*
        Some tests run without UI, so the async code listens to a message
        that can be sent programatically
      */
      let applyBlocklistChanges = () => {
        for (let addon of addonList) {
          if (!addon.disable)
            continue;

          if (addon.item instanceof Ci.nsIPluginTag)
            addon.item.enabledState = Ci.nsIPluginTag.STATE_DISABLED;
          else {
            // This add-on is softblocked.
            addon.item.softDisabled = true;
            // We must revert certain prefs.
            let prefs = this._getAddonPrefs(addon.item);
            resetPrefs(prefs);
          }
        }

        if (args.restart)
          restartApp();

        this._notifyObserversBlocklistUpdated();
        Services.obs.removeObserver(applyBlocklistChanges, "addon-blocklist-closed");
      }

      Services.obs.addObserver(applyBlocklistChanges, "addon-blocklist-closed", false);

      if (getPref("getBoolPref", PREF_BLOCKLIST_SUPPRESSUI, false)) {
        applyBlocklistChanges();
        return;
      }

      function blocklistUnloadHandler(event) {
        if (event.target.location == URI_BLOCKLIST_DIALOG) {
          applyBlocklistChanges();
          blocklistWindow.removeEventListener("unload", blocklistUnloadHandler);
        }
      }

      let blocklistWindow = Services.ww.openWindow(null, URI_BLOCKLIST_DIALOG, "",
                              "chrome,centerscreen,dialog,titlebar", args);
      if (blocklistWindow)
        blocklistWindow.addEventListener("unload", blocklistUnloadHandler, false);
    });
  },

  classID: Components.ID("{66354bc9-7ed1-4692-ae1d-8da97d6b205e}"),
  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
                                         Ci.nsIBlocklistService,
                                         Ci.nsITimerCallback]),
};

/**
 * Helper for constructing a blocklist.
 */
function BlocklistItemData(versionRangeElement) {
  var versionRange = this.getBlocklistVersionRange(versionRangeElement);
  this.minVersion = versionRange.minVersion;
  this.maxVersion = versionRange.maxVersion;
  if (versionRangeElement && versionRangeElement.hasAttribute("severity"))
    this.severity = versionRangeElement.getAttribute("severity");
  else
    this.severity = DEFAULT_SEVERITY;
  if (versionRangeElement && versionRangeElement.hasAttribute("vulnerabilitystatus")) {
    this.vulnerabilityStatus = versionRangeElement.getAttribute("vulnerabilitystatus");
  } else {
    this.vulnerabilityStatus = VULNERABILITYSTATUS_NONE;
  }
  this.targetApps = { };
  var found = false;

  if (versionRangeElement) {
    for (var i = 0; i < versionRangeElement.childNodes.length; ++i) {
      var targetAppElement = versionRangeElement.childNodes.item(i);
      if (!(targetAppElement instanceof Ci.nsIDOMElement) ||
          targetAppElement.localName != "targetApplication")
        continue;
      found = true;
      // default to the current application if id is not provided.
      var appID = targetAppElement.hasAttribute("id") ? targetAppElement.getAttribute("id") : gApp.ID;
      this.targetApps[appID] = this.getBlocklistAppVersions(targetAppElement);
    }
  }
  // Default to all versions of the current application when no targetApplication
  // elements were found
  if (!found)
    this.targetApps[gApp.ID] = this.getBlocklistAppVersions(null);
}

BlocklistItemData.prototype = {
  /**
   * Tests if a version of an item is included in the version range and target
   * application information represented by this BlocklistItemData using the
   * provided application and toolkit versions.
   * @param   version
   *          The version of the item being tested.
   * @param   appVersion
   *          The application version to test with.
   * @param   toolkitVersion
   *          The toolkit version to test with.
   * @returns True if the version range covers the item version and application
   *          or toolkit version.
   */
  includesItem: function(version, appVersion, toolkitVersion) {
    // Some platforms have no version for plugins, these don't match if there
    // was a min/maxVersion provided
    if (!version && (this.minVersion || this.maxVersion))
      return false;

    // Check if the item version matches
    if (!this.matchesRange(version, this.minVersion, this.maxVersion))
      return false;

    // Check if the application version matches
    if (this.matchesTargetRange(gApp.ID, appVersion))
      return true;

    // Check if the toolkit version matches
    return this.matchesTargetRange(TOOLKIT_ID, toolkitVersion);
  },

  /**
   * Checks if a version is higher than or equal to the minVersion (if provided)
   * and lower than or equal to the maxVersion (if provided).
   * @param   version
   *          The version to test.
   * @param   minVersion
   *          The minimum version. If null it is assumed that version is always
   *          larger.
   * @param   maxVersion
   *          The maximum version. If null it is assumed that version is always
   *          smaller.
   */
  matchesRange: function(version, minVersion, maxVersion) {
    if (minVersion && gVersionChecker.compare(version, minVersion) < 0)
      return false;
    if (maxVersion && gVersionChecker.compare(version, maxVersion) > 0)
      return false;
    return true;
  },

  /**
   * Tests if there is a matching range for the given target application id and
   * version.
   * @param   appID
   *          The application ID to test for, may be for an application or toolkit
   * @param   appVersion
   *          The version of the application to test for.
   * @returns True if this version range covers the application version given.
   */
  matchesTargetRange: function(appID, appVersion) {
    var blTargetApp = this.targetApps[appID];
    if (!blTargetApp)
      return false;

    for (let app of blTargetApp) {
      if (this.matchesRange(appVersion, app.minVersion, app.maxVersion))
        return true;
    }

    return false;
  },

  /**
   * Retrieves a version range (e.g. minVersion and maxVersion) for a
   * blocklist item's targetApplication element.
   * @param   targetAppElement
   *          A targetApplication blocklist element.
   * @returns An array of JS objects with the following properties:
   *          "minVersion"  The minimum version in a version range (default = null).
   *          "maxVersion"  The maximum version in a version range (default = null).
   */
  getBlocklistAppVersions: function(targetAppElement) {
    var appVersions = [ ];

    if (targetAppElement) {
      for (var i = 0; i < targetAppElement.childNodes.length; ++i) {
        var versionRangeElement = targetAppElement.childNodes.item(i);
        if (!(versionRangeElement instanceof Ci.nsIDOMElement) ||
            versionRangeElement.localName != "versionRange")
          continue;
        appVersions.push(this.getBlocklistVersionRange(versionRangeElement));
      }
    }
    // return minVersion = null and maxVersion = null if no specific versionRange
    // elements were found
    if (appVersions.length == 0)
      appVersions.push(this.getBlocklistVersionRange(null));
    return appVersions;
  },

  /**
   * Retrieves a version range (e.g. minVersion and maxVersion) for a blocklist
   * versionRange element.
   * @param   versionRangeElement
   *          The versionRange blocklist element.
   * @returns A JS object with the following properties:
   *          "minVersion"  The minimum version in a version range (default = null).
   *          "maxVersion"  The maximum version in a version range (default = null).
   */
  getBlocklistVersionRange: function(versionRangeElement) {
    var minVersion = null;
    var maxVersion = null;
    if (!versionRangeElement)
      return { minVersion: minVersion, maxVersion: maxVersion };

    if (versionRangeElement.hasAttribute("minVersion"))
      minVersion = versionRangeElement.getAttribute("minVersion");
    if (versionRangeElement.hasAttribute("maxVersion"))
      maxVersion = versionRangeElement.getAttribute("maxVersion");

    return { minVersion: minVersion, maxVersion: maxVersion };
  }
};

this.NSGetFactory = XPCOMUtils.generateNSGetFactory([Blocklist]);
