/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is the Extension Manager.
 *
 * The Initial Developer of the Original Code is Ben Goodger.
 * Portions created by the Initial Developer are Copyright (C) 2004
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *  Ben Goodger <ben@bengoodger.com>
 *  Benjamin Smedberg <benjamin@smedbergs.us>
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

const nsIExtensionManager             = Components.interfaces.nsIExtensionManager;
const nsIUpdateService                = Components.interfaces.nsIUpdateService;
const nsIUpdateItem                   = Components.interfaces.nsIUpdateItem;

const PREF_EM_APP_EXTENSIONS_VERSION  = "app.extensions.version";
const PREF_EM_LAST_APP_VERSION        = "extensions.lastAppVersion";
const PREF_UPDATE_COUNT               = "extensions.update.count";
const PREF_UPDATE_DEFAULT_URL         = "extensions.update.url";
const PREF_EM_WASINSAFEMODE           = "extensions.wasInSafeMode";
const PREF_EM_DISABLEDOBSOLETE        = "extensions.disabledObsolete";
const PREF_EM_LAST_SELECTED_SKIN      = "extensions.lastSelectedSkin";
const PREF_EM_EXTENSION_FORMAT        = "extensions.%UUID%.";
const PREF_EM_ITEM_UPDATE_ENABLED     = "extensions.%UUID%.update.enabled";
const PREF_EM_ITEM_UPDATE_URL         = "extensions.%UUID%.update.url";
const PREF_EM_DSS_ENABLED             = "extensions.dss.enabled";

const PREF_GENERAL_SKINS_SELECTEDSKIN = "general.skins.selectedSkin";

const DIR_EXTENSIONS                  = "extensions";
const DIR_UNINSTALL                   = "uninstall";
const DIR_TEMP                        = "temp";
const DIR_CHROME                      = "chrome";
const DIR_COMPONENTS                  = "components";
const DIR_DEFAULTS                    = "defaults";
const DIR_DEFAULTS_PREFS              = "preferences";
const DIR_DEFAULTS_EXTENSIONS         = "extensions"; 
const DIR_CR_CHROME                   = "chrome";
const DIR_CR_OVERLAYINFO              = "overlayinfo";
const FILE_CR_CHROMEDS                = "chrome.rdf";
const FILE_EXTENSIONS                 = "Extensions.rdf";
const FILE_UNINSTALL_LOG              = "Uninstall";
const FILE_DEFAULTS                   = "defaults.ini";
const FILE_COMPONENT_MANIFEST         = "components.ini";
const FILE_AUTOREG                    = ".autoreg";
const FILE_INSTALL_MANIFEST           = "install.rdf";
const FILE_CHROME_MANIFEST            = "contents.rdf";
const FILE_WASINSAFEMODE              = "Safe Mode";
const FILE_INSTALLED_EXTENSIONS       = "installed-extensions.txt"
const FILE_INSTALLED_EXTENSIONS_PROCESSED = "installed-extensions-processed.txt"
const TARGET_OS                                 = "WINNT";

const KEY_PROFILEDIR                  = "ProfD";
const KEY_APPDIR                      = "XCurProcD";
const KEY_DEFAULTS                    = "ProfDefNoLoc";
const KEY_DEFAULT_THEME               = "classic/1.0";

const ERROR_INVALID_VERSION           = -1;
const ERROR_PHONED_HOME               = -2;
const ERROR_EXTENSION_IS_THEME        = -3;

var gApp            = null;
var gPref           = null;
var gRDF            = null;
var gOS             = null;
var gVersionChecker = null;

function getVersionChecker()
{
  if (!gVersionChecker) {
    gVersionChecker = Components.classes["@mozilla.org/updates/version-checker;1"]
                                .getService(Components.interfaces.nsIVersionChecker);
  }
  return gVersionChecker;
}

///////////////////////////////////////////////////////////////////////////////
//
// Utility Functions
//
const EM_NS_PREFIX      = "http://www.mozilla.org/2004/em-rdf#";
const CHROME_NS_PREFIX  = "http://www.mozilla.org/rdf/chrome#";

function EM_NS(aProperty)
{
  return EM_NS_PREFIX + aProperty;
}

function CHROME_NS(aProperty)
{
  return CHROME_NS_PREFIX + aProperty;
}

// Returns the specified directory hierarchy under the special directory 
// specified by aKey, creating directories along the way if necessary.
function getDir(aKey, aSubDirs)
{
  return getDirInternal(aKey, aSubDirs, true);
}

function getDirNoCreate(aKey, aSubDirs)
{
  return getDirInternal(aKey, aSubDirs, false);
}

function getDirInternal(aKey, aSubDirs, aCreate)
{
  var fileLocator = Components.classes["@mozilla.org/file/directory_service;1"]
                              .getService(Components.interfaces.nsIProperties);
  var dir = fileLocator.get(aKey, Components.interfaces.nsIFile);
  for (var i = 0; i < aSubDirs.length; ++i) {
    dir.append(aSubDirs[i]);
    if (aCreate && !dir.exists())
      dir.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0755);
  }
  return dir;
}

// Returns the file at the appropriate point in a directory hierarchy under
// the specified key, creating directories along the way if necessary. Does
// NOT create the file.
function getFile(aKey, aPathToFile)
{
  var subdirs = [];
  for (var i = 0; i < aPathToFile.length - 1; ++i)
    subdirs.push(aPathToFile[i]);
  var file = getDir(aKey, subdirs);
  file.append(aPathToFile[aPathToFile.length - 1]);
  return file;
}

function getDirKey(aIsProfile)
{
  return aIsProfile ? KEY_PROFILEDIR : KEY_APPDIR;
}

function dumpFile(aFile)
{
  dump("*** file = " + aFile.path + ", exists = " + aFile.exists() + "\n");
}

// We use this to force RDF to bypass the cache when loading certain types
// of files. 
function getRandomFileName(aName, aExtension)
{
  var characters = "abcdefghijklmnopqrstuvwxyz0123456789";
  var nameString = aName + "-";
  for (var i = 0; i < 3; ++i) {
    var index = Math.round((Math.random()) * characters.length);
    nameString += characters.charAt(index);
  }
  return nameString + "." + aExtension;
}

const PREFIX_EXTENSION  = "urn:mozilla:extension:";
const PREFIX_THEME      = "urn:mozilla:theme:";
const ROOT_EXTENSION    = "urn:mozilla:extension:root";
const ROOT_THEME        = "urn:mozilla:theme:root";

function getItemPrefix(aItemType)
{
  var prefix = "";
  if (aItemType & nsIUpdateItem.TYPE_EXTENSION) 
    prefix = PREFIX_EXTENSION;
  else if (aItemType & nsIUpdateItem.TYPE_THEME)
    prefix = PREFIX_THEME;
  return prefix;
}

function getItemRoot(aItemType)
{
  var root = "";
  if (aItemType & nsIUpdateItem.TYPE_EXTENSION) 
    root = ROOT_EXTENSION;
  else if (aItemType & nsIUpdateItem.TYPE_THEME)
    root = ROOT_THEME;
  return root;
}

function getItemRoots(aItemType)
{    
  var roots = [];
  if (aItemType == nsIUpdateItem.TYPE_ADDON)
    roots = roots.concat([getItemRoot(nsIUpdateItem.TYPE_EXTENSION), 
                          getItemRoot(nsIUpdateItem.TYPE_THEME)]);
  else
    roots.push(getItemRoot(aItemType));
  return roots;
}

function getItemType(aURI)
{
  var type = -1;
  if (aURI.substr(0, PREFIX_EXTENSION.length) == PREFIX_EXTENSION)
    type = nsIUpdateItem.TYPE_EXTENSION;
  else if (aURI.substr(0, PREFIX_THEME.length) == PREFIX_THEME)
    type = nsIUpdateItem.TYPE_THEME;
  return type;
}

function stripPrefix(aURI, aItemType)
{
  var val = aURI;
  if (aItemType == nsIUpdateItem.TYPE_ADDON)
    val = stripPrefix(aURI, getItemType(aURI));
  else {
    var prefix = getItemPrefix(aItemType);
    if (prefix && aURI.substr(0, prefix.length) == prefix)  
      val = aURI.substr(prefix.length, aURI.length);
  }
  return val;
}

function stripPropertyPrefix(aProperty, aPrefix)
{
  return aProperty.substr(aPrefix.length, aProperty.length);
}

function getURLSpecFromFile(aFile)
{
  var ioServ = Components.classes["@mozilla.org/network/io-service;1"]
                          .getService(Components.interfaces.nsIIOService);
  var fph = ioServ.getProtocolHandler("file").QueryInterface(Components.interfaces.nsIFileProtocolHandler);
  return fph.getURLSpecFromFile(aFile);
}

function ensureExtensionsFiles(aIsProfile)
{
  try {
    var extensionsFile  = getFile(getDirKey(aIsProfile), 
                                  [DIR_EXTENSIONS, FILE_EXTENSIONS]);
  
    // If the file does not exist at the current location, copy the default
    // version over so we can access the various roots. 
    // This is a sign also that something may have gone wrong, such as the user
    // deleting /Extensions so we should remove the relative contents.rdf and
    // overlayinfo hierarchies too. 
    if (extensionsFile && !extensionsFile.exists()) {
      var defaultFile = getFile(KEY_DEFAULTS, 
                                [DIR_DEFAULTS_EXTENSIONS, FILE_EXTENSIONS]);
      defaultFile.copyTo(extensionsFile.parent, extensionsFile.leafName);

      // XXXben - do this only for profile until we have a better protection 
      // mechanism for global items.
      if (aIsProfile) {
        try {      
          var chromedsFile = getFile(getDirKey(aIsProfile), [DIR_CR_CHROME, FILE_CR_CHROMEDS]);
          if (chromedsFile.exists())
            chromedsFile.remove(false);
          var overlayinfoDir = getDir(getDirKey(aIsProfile), [DIR_CR_CHROME, DIR_CR_OVERLAYINFO]);
          if (overlayinfoDir.exists())
            overlayinfoDir.remove(true);
        }
        catch (e) { 
          dump("Extension System Warning: failed to remove chrome.rdf/overlay info because: " + e + "\n"); 
        }
      }
    }
  }
  catch (e) { 
    // Too early in the startup process to use the console, we may yet restart
    // the app.
    dump("Extension System Warning: Failed to set up default extensions" + 
         " files probably because you do not have write privileges to this" + 
         " location. While you can run Firefox like this, it is recommended" + 
         " that you run it at least once with privileges that allow it to generate" + 
         " these initial files to improve start performance. Running from a disk" + 
         " image on MacOS X is not recommended.");
  }
}

function stringData(aLiteralOrResource)
{
  try {
    var obj = aLiteralOrResource.QueryInterface(Components.interfaces.nsIRDFLiteral);
    return obj.Value;
  }
  catch (e) {
    try {
      obj = aLiteralOrResource.QueryInterface(Components.interfaces.nsIRDFResource);
      return obj.Value;
    }
    catch (e) {}
  }
  return "--";
}

function stackTraceFunctionFormat(aFunctionName)
{
  var classDelimiter = aFunctionName.indexOf("_");
  var className = aFunctionName.substr(0, classDelimiter);
  if (!className)
    className == "<global>";
  var functionName = aFunctionName.substr(classDelimiter + 1, aFunctionName.length);
  if (!functionName) 
    functionName == "<anonymous>";
  return className + "::" + functionName;
}

function stackTrace(aArguments, aMaxCount)
{
  dump("=[STACKTRACE]=====================================================\n");
  dump("*** at: " + stackTraceFunctionFormat(aArguments.callee.name) + "()\n");
  var temp = aArguments.callee.caller;
  var count = 0;
  while (temp) {
    dump("***     " + stackTraceFunctionFormat(temp.name) + "()\n");
    
    temp = temp.arguments.callee.caller;
    if (aMaxCount > 0 && ++count == aMaxCount)
      break;
  }
  dump("==================================================================\n");
}

///////////////////////////////////////////////////////////////////////////////
// Incompatible Item Error Message
function showIncompatibleError(aDS)
{
  var sbs = Components.classes["@mozilla.org/intl/stringbundle;1"]
                      .getService(Components.interfaces.nsIStringBundleService);
  var extensionStrings = sbs.createBundle("chrome://mozapps/locale/extensions/extensions.properties");
  var params = [extensionStrings.GetStringFromName("extension")];
  var title = extensionStrings.formatStringFromName("incompatibleTitle", 
                                                    params, params.length);
  
  var brandStrings = sbs.createBundle("chrome://global/locale/brand.properties");
  var brandShortName = brandStrings.GetStringFromName("brandShortName");

  var message;
  var metadata = {};
  getItemMetadata(aDS, metadata);
  
  if (undefined === metadata.minAppVersion) {
    // getItemMetadata does not fill target application version range properties unless a 
    // matching supported target application is found.
    params = [metadata.name, metadata.version, brandShortName];
    message = extensionStrings.formatStringFromName("incompatibleMessageNoApp", 
                                                    params, params.length);
  }
  else if (metadata.minAppVersion == metadata.maxAppVersion) {
    // If the min target app version and the max target app version are the same, don't show
    // a message like, "Foo is only compatible with Firefox versions 0.7 to 0.7", rather just
    // show, "Foo is only compatible with Firefox 0.7"
    params = [metadata.name, metadata.version, brandShortName, metadata.name, 
              metadata.version, brandShortName, metadata.minAppVersion];
    message = extensionStrings.formatStringFromName("incompatibleMessageSingleAppVersion", 
                                                    params, params.length);
  }
  else {
    params = [metadata.name, metadata.version, brandShortName, metadata.name, 
              metadata.version, brandShortName, metadata.minAppVersion, 
              metadata.maxAppVersion];
    message = extensionStrings.formatStringFromName("incompatibleMessage", params, params.length);
  }
  var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
                     .getService(Components.interfaces.nsIPromptService);
  ps.alert(null, title, message);
}

function showMalformedError(aFile)
{
  var sbs = Components.classes["@mozilla.org/intl/stringbundle;1"]
                      .getService(Components.interfaces.nsIStringBundleService);
  var extensionStrings = sbs.createBundle("chrome://mozapps/locale/extensions/extensions.properties");
  var title = extensionStrings.GetStringFromName("malformedTitle");

  var brandStrings = sbs.createBundle("chrome://global/locale/brand.properties");
  var brandShortName = brandStrings.GetStringFromName("brandShortName");
  var message = extensionStrings.formatStringFromName("malformedMessage", [brandShortName, aFile], 2);
  
  var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
                     .getService(Components.interfaces.nsIPromptService);
  ps.alert(null, title, message);
}

function showInvalidVersionError(aItemName, aVersion)
{
  var sbs = Components.classes["@mozilla.org/intl/stringbundle;1"]
                      .getService(Components.interfaces.nsIStringBundleService);
  var extensionStrings = sbs.createBundle("chrome://mozapps/locale/extensions/extensions.properties");
  var title = extensionStrings.GetStringFromName("invalidVersionTitle");

  var brandStrings = sbs.createBundle("chrome://global/locale/brand.properties");
  var brandShortName = brandStrings.GetStringFromName("brandShortName");
  var params = [brandShortName, aItemName, aVersion];
  var message = extensionStrings.formatStringFromName("invalidVersionMessage", params, params.length);
  
  var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
                     .getService(Components.interfaces.nsIPromptService);
  ps.alert(null, title, message);
}

function showOldThemeError(aDS)
{
  var sbs = Components.classes["@mozilla.org/intl/stringbundle;1"]
                      .getService(Components.interfaces.nsIStringBundleService);
  var extensionStrings = sbs.createBundle("chrome://mozapps/locale/extensions/extensions.properties");
  var params = [extensionStrings.GetStringFromName("theme")];
  var title = extensionStrings.formatStringFromName("incompatibleTitle", 
                                                    params, params.length);

  var brandStrings = sbs.createBundle("chrome://global/locale/brand.properties");
  var brandShortName = brandStrings.GetStringFromName("brandShortName");
  var appVersion = extensionStrings.GetStringFromName("incompatibleOlder");
  
  try {  
    var rdfc = Components.classes["@mozilla.org/rdf/container;1"]
                         .createInstance(Components.interfaces.nsIRDFContainer);
    rdfc.Init(aDS, gRDF.GetResource("urn:mozilla:skin:root"));
    
    var elts = rdfc.GetElements();
    var nameArc = gRDF.GetResource(CHROME_NS("displayName"));
    while (elts.hasMoreElements()) {
      var elt = elts.getNext().QueryInterface(Components.interfaces.nsIRDFResource);
      themeName = stringData(aDS.GetTarget(elt, nameArc, true));
      if (themeName) 
        break;
    }
  }
  catch (e) {
    themeName = extensionStrings.GetStringFromName("incompatibleThemeName");
  }
  
  params = [themeName, "", brandShortName, themeName, "", brandShortName, appVersion];
  var message = extensionStrings.formatStringFromName("incompatibleMessageSingleAppVersion",
                                                      params, params.length);
  var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
                     .getService(Components.interfaces.nsIPromptService);
  ps.alert(null, title, message);
}

function showMissingFileError(aSourceFile, aMissingFileName)
{
  var sbs = Components.classes["@mozilla.org/intl/stringbundle;1"]
                      .getService(Components.interfaces.nsIStringBundleService);
  var extensionStrings = sbs.createBundle("chrome://mozapps/locale/extensions/extensions.properties");
  var title = extensionStrings.GetStringFromName("missingFileTitle");

  var brandStrings = sbs.createBundle("chrome://global/locale/brand.properties");
  var brandShortName = brandStrings.GetStringFromName("brandShortName");
  var params = [brandShortName, aMissingFileName];
  var message = extensionStrings.formatStringFromName("missingFileMessage", params, params.length);
  
  var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
                     .getService(Components.interfaces.nsIPromptService);
  ps.alert(null, title, message);

  var consoleService = Components.classes["@mozilla.org/consoleservice;1"]
                                 .getService(Components.interfaces.nsIConsoleService);
  params = [aSourceFile, aMissingFileName];
  var consoleMessage = extensionStrings.formatStringFromName("missingFileConsoleMessage",
                                                             params, params.length);
  consoleService.logStringMessage(consoleMessage);
}

function showMalformedRegistrationError(aCRException)
{
  var sbs = Components.classes["@mozilla.org/intl/stringbundle;1"]
                      .getService(Components.interfaces.nsIStringBundleService);
  var extensionStrings = sbs.createBundle("chrome://mozapps/locale/extensions/extensions.properties");
  var title = extensionStrings.GetStringFromName("malformedRegistrationTitle");

  var brandStrings = sbs.createBundle("chrome://global/locale/brand.properties");
  var brandShortName = brandStrings.GetStringFromName("brandShortName");
  var params = [brandShortName];
  var message = extensionStrings.formatStringFromName("malformedRegistrationMessage", 
                                                      params, params.length);
  var detailsButtonMessage = extensionStrings.GetStringFromName("malformedRegistrationDetailsButton");
  
  var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
                     .getService(Components.interfaces.nsIPromptService);
  var flags = (ps.BUTTON_TITLE_IS_STRING * ps.BUTTON_POS_1) +  
              (ps.BUTTON_TITLE_OK * ps.BUTTON_POS_0);
  var result = ps.confirmEx(null, title, message, flags, null, detailsButtonMessage, null, null, { } );
  if (result == 1) {  
    var consoleService = Components.classes["@mozilla.org/consoleservice;1"]
                                  .getService(Components.interfaces.nsIConsoleService);
    params = [aCRException.extensionID, aCRException.functionName,
              aCRException.chromePath, aCRException.isProfile, ];
    var consoleMessage = extensionStrings.formatStringFromName("malformedRegistrationConsoleMessage",
                                                               params, params.length);
    consoleService.logStringMessage(consoleMessage);
    
    var ww = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
                        .getService(Components.interfaces.nsIWindowWatcher);
    ww.openWindow(null, "chrome://global/content/console.xul", 
                  "", "chrome,modal,centerscreen,resizable", null);
  }
}

function getItemMetadata(aDS, aResult)
{
  var manifestRoot = gRDF.GetResource("urn:mozilla:install-manifest");

  // Extension Name and Version
  var props = ["name", "version"];
  for (var i = 0; i < props.length; ++i) {
    var prop = gRDF.GetResource(EM_NS(props[i]));
    aResult[props[i]] = stringData(aDS.GetTarget(manifestRoot, prop, true));
  }
  
  // Target App Name and Version
  var appID = gApp.ID;

  var targets = aDS.GetTargets(manifestRoot, gRDF.GetResource(EM_NS("targetApplication")), true);
  var idRes = gRDF.GetResource(EM_NS("id"));
  var minVersionRes = gRDF.GetResource(EM_NS("minVersion"));
  var maxVersionRes = gRDF.GetResource(EM_NS("maxVersion"));
  while (targets.hasMoreElements()) {
    var targetApp = targets.getNext().QueryInterface(Components.interfaces.nsIRDFResource);
    var id          = stringData(aDS.GetTarget(targetApp, idRes, true));
    var minVersion  = stringData(aDS.GetTarget(targetApp, minVersionRes, true));
    var maxVersion  = stringData(aDS.GetTarget(targetApp, maxVersionRes, true));

    if (id == appID) {
      aResult.minAppVersion = minVersion;
      aResult.maxAppVersion = maxVersion;
      break;
    }
  }
}

function getInstallManifest(aFile)
{
  var fileURL = getURLSpecFromFile(aFile);
  var ds = gRDF.GetDataSourceBlocking(fileURL);
  var manifestRoot = gRDF.GetResource("urn:mozilla:install-manifest");
  var arcs = ds.ArcLabelsOut(manifestRoot);
  if (!arcs.hasMoreElements()) {
    ds = null;
    var uri = Components.classes["@mozilla.org/network/standard-url;1"]
                        .createInstance(Components.interfaces.nsIURI);
    uri.spec = fileURL;
    var url = uri.QueryInterface(Components.interfaces.nsIURL);
    showMalformedError(url.fileName);
  }
  return ds;
}

function ArrayEnumerator(aItems)
{
  this._index = 0;
  
  if (aItems) {
    for (var i = 0; i < aItems.length; ++i) {    
      if (!aItems[i])
        aItems.splice(i, 1);      
    }
  }
  
  this._contents = aItems;
}

ArrayEnumerator.prototype = {
  _index: 0,
  _contents: [],
  
  hasMoreElements: function ArrayEnumerator_hasMoreElements()
  {
    return this._index < this._contents.length;
  },
  
  getNext: function ArrayEnumerator_getNext()
  {
    return this._contents[this._index++];      
  }
};
  
///////////////////////////////////////////////////////////////////////////////
//
// nsInstallLogBase
//
function nsInstallLogBase()
{
}

nsInstallLogBase.prototype = {
  CHROME_TYPE_PACKAGE   : "package",
  CHROME_TYPE_SKIN      : "skin",
  CHROME_TYPE_LOCALE    : "locale",

  TOKEN_ADD_FILE        : "add",
  TOKEN_REGISTER_CHROME : "register",
  TOKEN_PROFILE         : "profile",
  TOKEN_GLOBAL          : "global",
  TOKEN_SKIN            : "skin"
};

///////////////////////////////////////////////////////////////////////////////
//
// nsInstallLogWriter
//
function nsInstallLogWriter(aExtensionID, aIsProfile)
{
  this._isProfile = aIsProfile;
  this._uninstallLog = getDir(getDirKey(aIsProfile),
                              [DIR_EXTENSIONS, aExtensionID, DIR_UNINSTALL]);
  this._uninstallLog.append(FILE_UNINSTALL_LOG);
}

nsInstallLogWriter.prototype = {
  __proto__       : nsInstallLogBase.prototype,
  _uninstallLog   : null,
  
  open: function nsInstallLogWriter_open ()
  {
    this._fos = Components.classes["@mozilla.org/network/file-output-stream;1"]
                          .createInstance(Components.interfaces.nsIFileOutputStream);
    const MODE_WRONLY   = 0x02;
    const MODE_CREATE   = 0x08;
    const MODE_TRUNCATE = 0x20;
    this._fos.init(this._uninstallLog, MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE, 0644, 0);
  },
  
  close: function nsInstallLogWriter_close ()
  {
    this._fos.close();  
  },
  
  addFile: function nsInstallLogWriter_addFile (aFile) 
  {
    var line = "add\t" + aFile.persistentDescriptor + "\n";
    this._fos.write(line, line.length);
  },
  
  registerChrome: function nsInstallLogWriter_registerChrome (aProviderName, aChromeType, aIsProfile)
  {
    var profile = aIsProfile ? "profile" : "global";
    // register\tprofile\tpackage\t<provider_name>
    var line = "register\t" + profile + "\t" + aChromeType + "\t" + aProviderName + "\n";
    this._fos.write(line, line.length);
  },
  
  installSkin: function nsInstallLogWriter_installSkin (aSkinName, aIsProfile)
  {
    var profile = aIsProfile ? "profile" : "global";
    // register\tprofile\tpackage\t<provider_name>
    var line = "skin\t" + profile + "\t" + aSkinName + "\n";
    this._fos.write(line, line.length);
  }
};

///////////////////////////////////////////////////////////////////////////////
//
// nsInstallLogReader
//
function nsInstallLogReader(aExtensionID, aIsProfile, aListener)
{
  this._isProfile = aIsProfile;
  this.uninstallLog = getFile(getDirKey(aIsProfile),
                              [DIR_EXTENSIONS, aExtensionID, 
                               DIR_UNINSTALL, FILE_UNINSTALL_LOG]);
  this._listener = aListener
}

nsInstallLogReader.prototype = {
  __proto__       : nsInstallLogBase.prototype,
  uninstallLog    : null,
  _listener       : null,
  
  read: function nsInstallLogReader_read ()
  {
    if (!this.uninstallLog.exists())
      return;
  
    var fis = Components.classes["@mozilla.org/network/file-input-stream;1"]
                        .createInstance(Components.interfaces.nsIFileInputStream);
    fis.init(this.uninstallLog, -1, -1, false);
    var lis = fis.QueryInterface(Components.interfaces.nsILineInputStream);
    var line = { value: "" };
    var more = false;
    var lines = [];
    do {
      more = lis.readLine(line);
      lines.push(line.value);
    }
    while (more);
    fis.close();

    // Now that we've closed the stream we can remove all the files, unregister
    // chrome, etc. 
    //
    // The list of lines we pass to the uninstall handler should be in this
    // order:
    // 1) File additions
    // 2) Chrome Package Registrations
    // 3) Chrome Skin and Locale Registrations
    //
    // They must be in this order since skins and locales rely on packages, and
    // the packages they rely on is not stored in the registration line so we
    // simply "deselect" for every package installed by the extension.
    var dependentLines = [];
    for (var i = 0; i < lines.length; ++i) {
      var parts = lines[i].split("\t");
      if (parts[1] == this.TOKEN_REGISTER_CHROME && 
          (parts[2] == this.CHROME_TYPE_SKIN || 
           parts[2] == this.CHROME_TYPE_LOCALE)) {
        dependentLines.push(lines.splice(i, 1));
      }
    }
    lines.concat(dependentLines);
    
    for (var i = 0; i < lines.length; ++i)
      this._parseLine(lines[i]);
  },
  
  _parseLine: function nsInstallLogReader__parseLine(aLine)
  {
    var parts = aLine.split("\t");
    switch (parts[0]) {
    case this.TOKEN_ADD_FILE:
      var prefix = this.TOKEN_ADD_FILE + "\t";
      var filePD = aLine.substr(prefix.length, aLine.length);
      var lf = Components.classes["@mozilla.org/file/local;1"]
                         .createInstance(Components.interfaces.nsILocalFile);
      try {
        lf.persistentDescriptor = filePD;
        this._listener.onAddFile(lf);
      }
      catch (e) { 
        dump("*** nsInstallLogReader::_parseLine - failed to remove file " + e + "\n"); 
      }
      break;
    case this.TOKEN_REGISTER_CHROME:
      var isProfile = parts[1] == this.TOKEN_PROFILE;
      try {
        this._listener.onRegisterChrome(parts[3], lf, parts[2], isProfile);
      } 
      catch (e) {
        dump("*** nsInstallLogReader::_parseLine - failed to deregister chrome\n"); 
      }
      break;
    case this.TOKEN_SKIN:
      var isProfile = parts[1] == this.TOKEN_PROFILE;
      try {
        this._listener.onInstallSkin(parts[2], isProfile);
      } 
      catch (e) {
        dump("*** nsInstallLogReader::_parseLine - failed to uninstall skin\n"); 
      }
      break;
    }
  }
};

///////////////////////////////////////////////////////////////////////////////
//
// nsInstalledExtensionReader
//
function nsInstalledExtensionReader(aManager)
{
  this._installedExtensions = getFile(KEY_APPDIR,
                                      [DIR_EXTENSIONS, 
                                       FILE_INSTALLED_EXTENSIONS]);
  this._installedExtensionsProcessed = getFile(KEY_APPDIR,
                                               [DIR_EXTENSIONS, 
                                                FILE_INSTALLED_EXTENSIONS_PROCESSED]);
  this._manager = aManager;
}

nsInstalledExtensionReader.prototype = {
  _manager            : null,
  _installedExtensions: null,
  
  read: function nsInstalledExtensionReader_read ()
  {
    if (this._installedExtensionsProcessed.exists())
      return;
    
    if (!this._installedExtensions.exists()) {
      var defaultsList = getFile(KEY_DEFAULTS, [DIR_DEFAULTS_EXTENSIONS, FILE_INSTALLED_EXTENSIONS]);
      defaultsList.copyTo(getDir(KEY_APPDIR, [DIR_EXTENSIONS]), FILE_INSTALLED_EXTENSIONS);
    }
      
    var fis = Components.classes["@mozilla.org/network/file-input-stream;1"]
                        .createInstance(Components.interfaces.nsIFileInputStream);
    fis.init(this._installedExtensions, -1, -1, false);
    var lis = fis.QueryInterface(Components.interfaces.nsILineInputStream);
    var line = { value: "" };
    var more = false;
    var lines = [];
    do {
      more = lis.readLine(line);
      lines.push(line.value);
    }
    while (more);
    fis.close();

    // Now that we've closed the stream we can remove all the files    
    for (var i = 0; i < lines.length; ++i)
      this._parseLine(lines[i]);
    
    this._installedExtensions.moveTo(getDir(KEY_APPDIR, [DIR_EXTENSIONS]), 
                                     FILE_INSTALLED_EXTENSIONS_PROCESSED);
  },
  
  TOKEN_EXTENSION : "extension",
  TOKEN_THEME     : "theme",
  
  _parseLine: function nsInstalledExtensionReader__parseLine (aLine)
  {
    // extension,{GUID} or theme,{GUID}
    var parts = aLine.split(",");
    var manifest = getFile(KEY_APPDIR, 
                           [DIR_EXTENSIONS, parts[1], FILE_INSTALL_MANIFEST]);
    if (parts.length != 2)
      return;
      
    if (!manifest.exists()) {
      defaultManifest = defaultFile = getFile(KEY_DEFAULTS, 
                                              [DIR_DEFAULTS_EXTENSIONS, parts[1], FILE_INSTALL_MANIFEST]);
      var extensionDir = getDir(KEY_APPDIR, [DIR_EXTENSIONS, parts[1]]);
      defaultManifest.copyTo(extensionDir, FILE_INSTALL_MANIFEST);
      manifest = getFile(KEY_APPDIR, 
                         [DIR_EXTENSIONS, parts[1], FILE_INSTALL_MANIFEST]);
    }
    switch (parts[0]) {
    case this.TOKEN_EXTENSION:
      this._manager.ensurePreConfiguredItem(parts[1], nsIUpdateItem.TYPE_EXTENSION, manifest);
      break;
    case this.TOKEN_THEME:
      this._manager.ensurePreConfiguredItem(parts[1], nsIUpdateItem.TYPE_THEME, manifest);
      break;
    }
  }
};

///////////////////////////////////////////////////////////////////////////////
//
// nsChromeRegistrationException
//
function nsChromeRegistrationException(aChromePath, aIsProfile, aFunctionName)
{
  this.chromePath = aChromePath;
  this.isProfile = aIsProfile;
  this.functionName = aFunctionName;
}
nsChromeRegistrationException.prototype = {
  chromePath      : null,
  isProfile       : true,
  functionName    : null,
  extensionID     : null
};

///////////////////////////////////////////////////////////////////////////////
//
// nsExtensionInstaller
//
function nsExtensionInstaller (aExtensionDS)
{
  this._extensionDS = aExtensionDS;

  this._provTypePackage = gRDF.GetResource(EM_NS("package"));
  this._provTypeSkin    = gRDF.GetResource(EM_NS("skin"));
  this._provTypeLocale  = gRDF.GetResource(EM_NS("locale"));
  this._fileProperty    = gRDF.GetResource(EM_NS("file"));
  this._sourceResource  = gRDF.GetResource("urn:mozilla:install-manifest");
}

nsExtensionInstaller.prototype = {
  // Utility services and helpers
  _rdf              : null,
  _writer           : null,

  // Extension metadata
  _extensionID      : null,
  _isProfile        : true,
  _extDirKey        : KEY_PROFILEDIR,
  
  // Source and target datasources
  _metadataDS       : null,
  _extensionDS      : null,
  
  // RDF objects and properties
  _provTypePackage  : null,
  _provTypeSkin     : null,
  _provTypeLocale   : null,
  _sourceResource   : null,
  _fileProperty     : null,
  
  install: function nsExtensionInstaller_install (aExtensionID, aIsProfile)
  {
    // Initialize the installer for this extension
    this._extensionID = aExtensionID;
    this._isProfile = aIsProfile;
    this._extDirKey = getDirKey(this._isProfile);

    // Create a logger to log install operations for uninstall
    this._writer = new nsInstallLogWriter(this._extensionID, this._isProfile);
    this._writer.open();
    
    // Move files from the staging dir into the extension's final home.
    // This function generates uninstall log files and creates backups of
    // existing files. 
    // XXXben - would like to add exception handling here to test for file
    //          I/O failures on uninstall log so that if there's a crash
    //          and the uninstall log is incorrectly/incompletely written 
    //          we can roll back. It's not critical that we do so right now
    //          since if this throws the extension's chrome is never 
    //          registered. 
    this._installExtensionFiles();
    
    // Load the metadata datasource
    var metadataFile = getFile(this._extDirKey, 
                               [DIR_EXTENSIONS, aExtensionID, FILE_INSTALL_MANIFEST]);
    
    this._metadataDS = getInstallManifest(metadataFile);
    if (!this._metadataDS) return;
    
    // Add metadata for the extension to the global extension metadata set
    this._extensionDS.addItemMetadata(this._extensionID, nsIUpdateItem.TYPE_EXTENSION, 
                                      this._metadataDS, this._isProfile);
    
    // Register chrome packages for files specified in the extension manifest
    try {
      this._registerChromeForExtension();
    }
    catch (e) {
      // Failed to register chrome, for any number of reasons - non-existent 
      // contents.rdf file at the location specified, malformed contents.rdf, 
      // etc. Set the "toBeUninstalled" flag so that the extension is uninstalled
      // properly during the subsequent uninstall pass in 
      // |nsExtensionManager::_finalizeOperations|

      this._extensionDS.setItemProperty(this._extensionID, 
                                        this._extensionDS._emR("toBeUninstalled"),
                                        this._extensionDS._emL("true"), this._isProfile,
                                        nsIUpdateItem.TYPE_EXTENSION);
      e.extensionID = this._extensionID;
      showMalformedRegistrationError(e);
    }
    
    this._writer.close();

    // Unset the "toBeInstalled" flag
    this._extensionDS.setItemProperty(this._extensionID, 
                                      this._extensionDS._emR("toBeInstalled"),
                                      null, this._isProfile,
                                      nsIUpdateItem.TYPE_EXTENSION);
  },
  
  _installExtensionFiles: function nsExtensionInstaller__installExtensionFiles ()
  {
    var sourceXPI = getFile(this._extDirKey, 
                            [DIR_EXTENSIONS, DIR_TEMP, 
                             this._extensionID, 
                             this._extensionID + ".xpi"]);
    var zipReader = Components.classes["@mozilla.org/libjar/zip-reader;1"]
                              .createInstance(Components.interfaces.nsIZipReader);
    zipReader.init(sourceXPI);
    zipReader.open();

    var entries = zipReader.findEntries("*");
    while (entries.hasMoreElements()) {
      var entry = entries.getNext().QueryInterface(Components.interfaces.nsIZipEntry);
      
      var parts = entry.name.split("/");
      var subDirs = [DIR_EXTENSIONS, this._extensionID];
      for (var i = 0; i < parts.length; ++i)
        subDirs.push(parts[i]);
      
      var fileName = parts[parts.length-1];
      if (fileName != "") {
        var targetFile = getFile(this._extDirKey, subDirs);
        zipReader.extract(entry.name, targetFile);
        this._writer.addFile(targetFile.QueryInterface(Components.interfaces.nsILocalFile));
      }
    }
    zipReader.close();
    // Kick off the extraction on a new thread, then join to wait for it to
    // complete. 
    // (new nsJarFileExtractor(aZipReader.file, dir)).extract();
    
    this._cleanUpStagedXPI();
  },
  
  _cleanUpStagedXPI: function nsExtensionInstaller__cleanUpStagedXPI ()
  {
    var stageDir = getDir(this._extDirKey, 
                          [DIR_EXTENSIONS, DIR_TEMP, this._extensionID]);
    var sourceXPI = stageDir.clone();
    sourceXPI.append(this._extensionID + ".xpi");
    sourceXPI.remove(false);
    
    // Remove the extension's stage dir
    if (!stageDir.directoryEntries.hasMoreElements()) 
      stageDir.remove(false);
      
    // If the parent "temp" dir is empty, remove it.
    try { // XXXben
      if (!stageDir.parent.directoryEntries.hasMoreElements())
        stageDir.parent.remove(false);
    }
    catch (e) { }
  },
  
  _registerChromeForExtension: function nsExtensionInstaller__registerChromeForExtension ()
  {
    // Enumerate the metadata datasource files collection and register chrome
    // for each file, calling _registerChrome for each.
    var chromeDir = getDir(this._extDirKey, 
                           [DIR_EXTENSIONS, this._extensionID, DIR_CHROME]);
    
    var files = this._metadataDS.GetTargets(this._sourceResource, this._fileProperty, true);
    while (files.hasMoreElements()) {
      var file = files.getNext().QueryInterface(Components.interfaces.nsIRDFResource);
      var chromeFile = chromeDir.clone();
      var fileName = file.Value.substr("urn:mozilla:extension:file:".length, file.Value.length);
      chromeFile.append(fileName);
      
      var providers = [this._provTypePackage, this._provTypeSkin, this._provTypeLocale];
      for (var i = 0; i < providers.length; ++i) {
        var items = this._metadataDS.GetTargets(file, providers[i], true);
        while (items.hasMoreElements()) {
          var item = items.getNext().QueryInterface(Components.interfaces.nsIRDFLiteral);
          this._registerChrome(chromeFile, providers[i], item.Value);
        }
      }
    }
  },
  
  _registerChrome: function nsExtensionInstaller__registerChrome (aFile, aChromeType, aPath)
  { 
    var fileURL = getURLSpecFromFile(aFile);
    if (!aFile.isDirectory()) // .jar files
      fileURL = "jar:" + fileURL + "!/" + aPath;
    else                      // flat chrome hierarchies
      fileURL = fileURL + aPath;
    
    var cr = Components.classes["@mozilla.org/chrome/chrome-registry;1"]
                       .getService(Components.interfaces.nsIXULChromeRegistry);
    var type;
    if (aChromeType.EqualsNode(this._provTypePackage)) {
      try {
        cr.installPackage(fileURL, this._isProfile);
      }
      catch (e) {
        throw new nsChromeRegistrationException(fileURL, this._isProfile, "installPackage");
      }
      type = this._writer.CHROME_TYPE_PACKAGE;
    }
    else if (aChromeType.EqualsNode(this._provTypeSkin)) {
      try {
        cr.installSkin(fileURL, this._isProfile, true); // Extension skins can execute scripts
      }
      catch (e) {
        throw new nsChromeRegistrationException(fileURL, this._isProfile, "installSkin");
      }
      type = this._writer.CHROME_TYPE_SKIN;
    }
    else if (aChromeType.EqualsNode(this._provTypeLocale)) {
      try {
        cr.installLocale(fileURL, this._isProfile);
      }
      catch (e) {
        throw new nsChromeRegistrationException(fileURL, this._isProfile, "installLocale");
      }
      type = this._writer.CHROME_TYPE_LOCALE;
    }
    var providerNames = this._getProviderNames(fileURL, type);
    for (var i = 0; i < providerNames.length; ++i) {
      this._writer.registerChrome(providerNames[i], type, this._isProfile);
      
      // Make sure we enable overlays for this extension so that if it is disabled by
      // mismatch checking, installing a newer version (as opposed to enabling as a 
      // result of a version compatibility update) makes the extension's overlaid UI 
      // appear immediately.
      cr.setAllowOverlaysForPackage(providerNames[i], true);
    }
  },
  
  _getProviderNames: function nsExtensionInstaller__getProviderNames (aBaseURL, aType)
  {
    if (aBaseURL.charAt(aBaseURL.length-1) != "/")
      aBaseURL += "/";
    var manifestURL = aBaseURL + "contents.rdf";
    
    var providerNames = [];

    try {
      // Discover the list of provider names to register for the location 
      // specified in the provider arc.
      //
      // The contents.rdf file will look like this:
      //
      //   <RDF:Seq about="urn:mozilla:<type>:root">
      //     <RDF:li resource="urn:mozilla:<type>:itemName1"/>
      //     <RDF:li resource="urn:mozilla:<type>:itemName2"/>
      //     ..
      //   </RDF:Seq>
      //
      // We need to explicitly walk this list here, we don't need to do so
      // for nsIXULChromeRegistry's |installPackage| method since that does
      // this same thing itself.
      
      var ds = gRDF.GetDataSourceBlocking(manifestURL);
      
      var ctr = Components.classes["@mozilla.org/rdf/container;1"]
                          .createInstance(Components.interfaces.nsIRDFContainer);
      ctr.Init(ds, gRDF.GetResource("urn:mozilla:" + aType + ":root"));
      
      var items = ctr.GetElements();
      while (items.hasMoreElements()) {
        var item = items.getNext().QueryInterface(Components.interfaces.nsIRDFResource);
        
        var nameArc = gRDF.GetResource(CHROME_NS("name"));
        var name;
        if (ds.hasArcOut(item, nameArc))
          name = stringData(ds.GetTarget(item, nameArc, true));
        else {
          var parts = item.Value.split(":");
          name = parts[parts.length-1];
        }
        providerNames.push(name);
      }
    }
    catch (e) { }
    
    return providerNames;
  }
};

///////////////////////////////////////////////////////////////////////////////
//
// nsExtensionUninstaller
//
function nsExtensionUninstaller(aExtensionDS)
{
  this._cr = Components.classes["@mozilla.org/chrome/chrome-registry;1"]
                       .getService(Components.interfaces.nsIXULChromeRegistry);
  this._extensionDS = aExtensionDS;
}

nsExtensionUninstaller.prototype = {
  _extensionDS  : null,
  _cr           : null,
  _isProfile    : true,
  _extDirKey    : "",
  _extensionsDir: null,
  _extensionID  : "",

  uninstall: function nsExtensionUninstaller_uninstall (aExtensionID, aIsProfile)
  {
    // Initialize the installer for this extension
    this._extensionID = aExtensionID;
    this._isProfile = aIsProfile;
    this._extDirKey = getDirKey(this._isProfile);
    this._extensionsDir = getDir(this._extDirKey, [DIR_EXTENSIONS]);

    // Create a logger to log install operations for uninstall
    this._reader = new nsInstallLogReader(this._extensionID, 
                                          this._isProfile, 
                                          this);
    try { // XXXben don't let errors stop us. 
      this._reader.read();
      
      // Now remove the uninstall log file. 
      this._removeFile(this._reader.uninstallLog);
    }
    catch (e) {
      dump("******* Failed to remove extension uninstall log, with exception = " + e + "\n");
    }
    
    // Unset the "toBeUninstalled" flag
    this._extensionDS.setItemProperty(this._extensionID, 
                                      this._extensionDS._emR("toBeUninstalled"),
                                      null, this._isProfile,
                                      nsIUpdateItem.TYPE_EXTENSION);
  },
  
  ///////////////////////////////////////////////////////////////////////////////
  // nsIInstallLogReaderListener
  onAddFile: function nsExtensionUninstaller_onAddFile (aFile)
  {
    this._removeFile(aFile);
  },
  
  _removeFile: function nsExtensionUninstaller__removeFile (aFile)
  {
    if (aFile.exists()) {
      aFile.remove(false);
      
      // Clean up the parent hierarchy if possible  
      var parent = aFile.parent;
      var e = parent.directoryEntries;
      if (!e.hasMoreElements() && 
          !parent.equals(this._extensionsDir)) // stop at the extensions dir
        this._removeFile(parent);
    }
  },
  
  // XXXben - maybe we should find a way to 
  _packagesForExtension: [],
  
  onRegisterChrome: function nsExtensionUninstaller_onRegisterChrome (aProviderName, aFile, aChromeType, aIsProfile)
  {
    switch (aChromeType) {
    case this._reader.CHROME_TYPE_PACKAGE:
      this._packagesForExtension.push(aProviderName);
      this._cr.uninstallPackage(aProviderName, aIsProfile)
      break;
    case this._reader.CHROME_TYPE_SKIN:
      for (var i = 0; i < this._packagesForExtension.length; ++i) {
        this._cr.deselectSkinForPackage(aProviderName, 
                                        this._packagesForExtension[i], 
                                        aIsProfile);
      }
      // this._cr.uninstallSkin(aProviderName, aIsProfile)
      break;
    case this._reader.CHROME_TYPE_LOCALE:
      for (var i = 0; i < this._packagesForExtension.length; ++i) {
        this._cr.deselectLocaleForPackage(aProviderName, 
                                          this._packagesForExtension[i], 
                                          aIsProfile);
      }
      // this._cr.uninstallLocale(aProviderName, aIsProfile)
      break;
    }
  }
};

///////////////////////////////////////////////////////////////////////////////
//
// nsExtensionEnabler
//
function nsExtensionEnabler(aExtensionDS)
{
  this._cr = Components.classes["@mozilla.org/chrome/chrome-registry;1"]
                       .getService(Components.interfaces.nsIXULChromeRegistry);
  this._extensionDS = aExtensionDS;
}

nsExtensionEnabler.prototype = {
  _extensionDS  : null,
  _cr           : null,
  _enable       : true,
  _isProfile    : true,
  _extDirKey    : "",
  _extensionsDir: null,

  enable: function nsExtensionEnabler_enable (aExtensionID, aIsProfile, aDisable)
  {
    // Initialize the installer for this extension
    this._enable = !aDisable;
    this._extensionID = aExtensionID;
    this._isProfile = aIsProfile;
    this._extDirKey = getDirKey(this._isProfile);
    this._extensionsDir = getDir(this._extDirKey, [DIR_EXTENSIONS]);

    // Create a logger to log install operations for uninstall
    this._reader = new nsInstallLogReader(this._extensionID, 
                                          this._isProfile, 
                                          this);
    this._reader.read();
  },
  
  onRegisterChrome: function nsExtensionEnabler_onRegisterChrome (aProviderName, aFile, aChromeType, aIsProfile)
  {
    if (aChromeType == this._reader.CHROME_TYPE_PACKAGE)
      this._cr.setAllowOverlaysForPackage(aProviderName, this._enable);
  },
  
  onAddFile: function nsExtensionEnabler_onAddFile (aFile)
  {
  }
};

///////////////////////////////////////////////////////////////////////////////
//
// nsThemeInstaller
//
function nsThemeInstaller(aExtensionDS, aManager)
{
  this._extensionDS = aExtensionDS;
  this._em = aManager;
}

nsThemeInstaller.prototype = {
  _extensionDS  : null,
  _isProfile    : true,
  _extDirKey    : "",

  install: function nsThemeInstaller_install (aJARFile, aIsProfile)
  {
    var extDirKey = getDirKey(aIsProfile);

    // Since we're installing a "new type" theme, we assume a file layout
    // within the JAR like so:
    // foo.jar/
    //         install.rdf      <-- Theme Manager metadata
    //         contents.rdf   <-- Chrome Registry metadata
    //         browser/
    //         global/
    //         ...
    var zipReader = Components.classes["@mozilla.org/libjar/zip-reader;1"]
                              .createInstance(Components.interfaces.nsIZipReader);
    zipReader.init(aJARFile);
    zipReader.open();
    
    try {
      zipReader.getEntry(FILE_INSTALL_MANIFEST);
    }
    catch (e) {
      // If the zip reader returned an error code here it means that the install.rdf
      // file was not found in the theme jar file - i.e. it was an old style theme. 
      // There's no reason for people to be installing or maintaining such themes 
      // anymore as there is no practical use for them, so we should throw an error
      // here and bail. 
      try {
        zipReader.getEntry(FILE_CHROME_MANIFEST);
        
        // Load the contents.rdf file from the .jar file if present and show a detailed
        // error.         
        var tempChromeManifest = getFile(extDirKey,
                                         [DIR_EXTENSIONS, DIR_TEMP, FILE_CHROME_MANIFEST]);
        zipReader.extract(FILE_CHROME_MANIFEST, tempChromeManifest);
        var rdfs = Components.classes["@mozilla.org/rdf/rdf-service;1"]
                             .getService(Components.interfaces.nsIRDFService);
        showOldThemeError(rdfs.GetDataSourceBlocking(getURLSpecFromFile(tempChromeManifest)));
        tempChromeManifest.remove(false);
      }
      catch (e) {
        showMissingFileError(aJARFile, FILE_CHROME_MANIFEST);
      }
    }
    
    var themeManifest = getFile(extDirKey,
                                [DIR_EXTENSIONS, DIR_TEMP, getRandomFileName("install", "rdf")]);
    zipReader.extract(FILE_INSTALL_MANIFEST, themeManifest);
    
    var chromeManifest = getFile(extDirKey,
                                 [DIR_EXTENSIONS, DIR_TEMP, FILE_CHROME_MANIFEST]);
    zipReader.extract(FILE_CHROME_MANIFEST, chromeManifest);
    
    var themeMetadata = getInstallManifest(themeManifest);
    if (!themeMetadata) return;
    var chromeMetadata = gRDF.GetDataSourceBlocking(getURLSpecFromFile(chromeManifest));
    
    // We do a basic version check first just to make sure we somehow weren't 
    // tricked into installing an incompatible theme...
    this._themeID = this._em.canInstallItem(themeMetadata);
    if (isNaN(parseInt(this._themeID))) {
      var canInstall = true;

      // Copy the file to its final location
      var destinationDir = getDir(extDirKey, 
                                  [DIR_EXTENSIONS, this._themeID, DIR_CHROME]);
      var destinationFile = destinationDir.clone();
      destinationFile.append(aJARFile.leafName);
      if (destinationFile.exists())
        destinationFile.remove(false);
      aJARFile.copyTo(destinationDir, aJARFile.leafName);

      var nameArc = gRDF.GetResource(CHROME_NS("name"));
      var ctr = Components.classes["@mozilla.org/rdf/container;1"]
                          .createInstance(Components.interfaces.nsIRDFContainer);
      ctr.Init(chromeMetadata, gRDF.GetResource("urn:mozilla:skin:root"));
      
      var elts = ctr.GetElements();
      while (elts.hasMoreElements()) {
        var elt = elts.getNext().QueryInterface(Components.interfaces.nsIRDFResource);
        var chromeSkinPrefix = "urn:mozilla:skin:";
        if (elt.Value.substr(0, chromeSkinPrefix.length) == chromeSkinPrefix) {
          var name = chromeMetadata.GetTarget(elt, nameArc, true);
          
          // Check to see if the em:internalName property on the theme install
          // manifest matches the chrome:name property on the theme's CR entry. 
          var manifestRoot = gRDF.GetResource("urn:mozilla:install-manifest");
          var internalName = themeMetadata.GetTarget(manifestRoot, 
                                                     gRDF.GetResource(EM_NS("internalName")),
                                                     true);
          if (!internalName.EqualsNode(name)) {
            var consoleService = Components.classes["@mozilla.org/consoleservice;1"]
                                           .getService(Components.interfaces.nsIConsoleService);
            consoleService.logStringMessage("Could not install theme because chrome:name arc in " +
                                            "the theme's contents.rdf file (" + stringData(name) +
                                            ") does not match the em:internalName arc in the theme's " + 
                                            "install.rdf file (" + stringData(internalName) + ")");
            var fileURL = getURLSpecFromFile(themeManifest);
            var uri = Components.classes["@mozilla.org/network/standard-url;1"]
                                .createInstance(Components.interfaces.nsIURI);
            uri.spec = fileURL;
            var url = uri.QueryInterface(Components.interfaces.nsIURL);
            showMalformedError(url.fileName);
            
            destinationFile.remove(false);
            destinationDir.remove(true);
            this._em._cleanDirs();
            
            canInstall = false;
          }

          if (canInstall) {
            name = name.QueryInterface(Components.interfaces.nsIRDFLiteral).Value;

            // Create a logger to log install operations for uninstall
            this._writer = new nsInstallLogWriter(this._themeID, aIsProfile);
            this._writer.open();
            this._writer.installSkin(name, aIsProfile);
          }
        }
      }

      if (canInstall) {
        this._writer.addFile(destinationFile.QueryInterface(Components.interfaces.nsILocalFile));
        this._writer.close();

        // Use the Chrome Registry API to install the theme there
        var filePath = "jar:" + getURLSpecFromFile(destinationFile) + "!/";      
        var cr = Components.classes["@mozilla.org/chrome/chrome-registry;1"]
                          .getService(Components.interfaces.nsIXULChromeRegistry);
        cr.installSkin(filePath, aIsProfile, false);

        // Insert the theme into the theme list. 
        this._extensionDS.insertForthcomingItem(this._themeID, nsIUpdateItem.TYPE_THEME, 
                                                aIsProfile);

        // Add metadata for the extension to the global extension metadata set
        this._extensionDS.addItemMetadata(this._themeID, nsIUpdateItem.TYPE_THEME,
                                          themeMetadata, aIsProfile);
      }
      
      this._extensionDS.doneInstallingTheme(this._themeID);
    }
    else if (this._themeID == 0)
      showIncompatibleError(themeMetadata);
    
    zipReader.close();
    themeManifest.remove(false);
    chromeManifest.remove(false);
  }
};

///////////////////////////////////////////////////////////////////////////////
//
// nsThemeUninstaller
//
function nsThemeUninstaller(aExtensionDS)
{
  this._cr = Components.classes["@mozilla.org/chrome/chrome-registry;1"]
                       .getService(Components.interfaces.nsIXULChromeRegistry);
}

nsThemeUninstaller.prototype = {
  _extensionsDir : null,
  
  uninstall: function nsThemeUninstaller_uninstall (aThemeID, aIsProfile)
  {
    this._extensionsDir = getDir(getDirKey(aIsProfile), [DIR_EXTENSIONS]);

    // Create a logger to log install operations for uninstall
    this._reader = new nsInstallLogReader(aThemeID, aIsProfile, this);
    try { // XXXben don't let errors stop us. 
      this._reader.read();
      
      // Now remove the uninstall log file. 
      this._removeFile(this._reader.uninstallLog);
    }
    catch (e) {
      dump("******* Failed to remove theme uninstall log, with exception = " + e + "\n");
    }
  },
  
  ///////////////////////////////////////////////////////////////////////////////
  // nsIInstallLogReaderListener
  onAddFile: function nsThemeUninstaller_onAddFile (aFile)
  {
    this._removeFile(aFile);
  },
  
  _removeFile: function nsThemeUninstaller__removeFile (aFile)
  {
    if (aFile.exists()) {
      aFile.remove(false);
      
      // Clean up the parent hierarchy if possible  
      var parent = aFile.parent;
      var e = parent.directoryEntries;
      if (!e.hasMoreElements() && 
          !parent.equals(this._extensionsDir)) // stop at the extensions dir
        this._removeFile(parent);
    }
  },
  
  onInstallSkin: function nsThemeUninstaller_onInstallSkin (aSkinName, aIsProfile)
  {
    this._cr.uninstallSkin(aSkinName, aIsProfile);
  }
};

///////////////////////////////////////////////////////////////////////////////
//
// nsExtensionManager
//
function nsExtensionManager()
{
  gApp = Components.classes["@mozilla.org/xre/app-info;1"]
                  .getService(Components.interfaces.nsIXULAppInfo);
  gPref = Components.classes["@mozilla.org/preferences-service;1"]
                    .getService(Components.interfaces.nsIPrefBranch);
  gRDF = Components.classes["@mozilla.org/rdf/rdf-service;1"]
                   .getService(Components.interfaces.nsIRDFService);
  gOS = Components.classes["@mozilla.org/observer-service;1"]
                  .getService(Components.interfaces.nsIObserverService);

  gOS.addObserver(this, "xpcom-shutdown", false);

  ensureExtensionsFiles(false);
}

nsExtensionManager.prototype = {
  _extInstaller     : null,
  _extUninstaller   : null,
  _extEnabler       : null,
  _started          : false,
  
  /////////////////////////////////////////////////////////////////////////////
  // nsIObserver
  observe: function nsExtensionManager_observe (aSubject, aTopic, aData)
  {
    switch (aTopic) {
    case "quit-application-requested":
      if (this._downloadCount > 0) {
        var result;
        result = this._confirmCancelDownloads(this._downloadCount, 
                                              "quitCancelDownloadsAlertTitle",
                                              "quitCancelDownloadsAlertMsgMultiple",
                                              "quitCancelDownloadsAlertMsg",
                                              "dontQuitButtonWin");
        if (!result)
          this._cancelDownloads();
        var PRBool = aSubject.QueryInterface(Components.interfaces.nsISupportsPRBool);
        PRBool.data = result;
      }
      break;
    case "offline-requested":
      if (this._downloadCount > 0) {
        result = this._confirmCancelDownloads(this._downloadCount,
                                              "offlineCancelDownloadsAlertTitle",
                                              "offlineCancelDownloadsAlertMsgMultiple",
                                              "offlineCancelDownloadsAlertMsg",
                                              "dontGoOfflineButton");
        if (!result)
          this._cancelDownloads();
        var PRBool = aSubject.QueryInterface(Components.interfaces.nsISupportsPRBool);
        PRBool.data = result;
      }
      break;  
    case "xpcom-shutdown":
      gOS.removeObserver(this, "xpcom-shutdown");    

      // Release strongly held services.
      gPref           = null;
      gRDF            = null;
      gOS             = null;
      gVersionChecker = null;
      break;
    }
  },
  
  start: function nsExtensionManager_start (aCmdLine, aIsDirty)
  {
    this._started = true;
    
    var needsRestart = false;
  
    ensureExtensionsFiles(true);
    
    // Somehow the component list went away, and for that reason the new one
    // generated by this function is going to result in a different compreg.
    // We must force a restart.
    var componentList = getFile(KEY_PROFILEDIR, [FILE_COMPONENT_MANIFEST]);
    if (!componentList.exists())
      needsRestart = true;
    
    // XXXben - a bit of a hack - clean up any empty dirs that may not have been
    //          properly removed by [un]install... I should really investigate those
    //          cases to see what is stopping these dirs from being removed, but no
    //          time now.
    this._cleanDirs();

    var safeMode = aCmdLine.handleFlag("safe-mode", false);
    if (!safeMode) {
      var wasInSafeModeFile = getFile(KEY_PROFILEDIR, [DIR_EXTENSIONS, FILE_WASINSAFEMODE]);
      if (wasInSafeModeFile.exists()) {
        // Clean up after we were in safe mode
        var win = this._showProgressWindow();
        try {
          this._ensureDS();
          
          // Retrieve the skin that was selected prior to entering safe mode
          // and select it. 
          var lastSelectedSkin = KEY_DEFAULT_THEME;
          try {
            lastSelectedSkin = gPref.getCharPref(PREF_EM_LAST_SELECTED_SKIN);
            gPref.clearUserPref(PREF_EM_LAST_SELECTED_SKIN);
            gPref.setCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN, lastSelectedSkin);
          } 
          catch (e) { }
          
          // Walk the list of extensions and re-activate overlays for packages 
          // that aren't disabled.
          var items = this._ds.getItemsWithFlagUnset("disabled", nsIUpdateItem.TYPE_EXTENSION);
          for (var i = 0; i < items.length; ++i)
            this._finalizeEnableDisable(items[i], false);
            
          wasInSafeModeFile.remove(false);
          
          this._writeDefaults(true);
          try {
            this._writeDefaults(false);
          }
          catch (e) { }
        }
        catch (e) {
          dump("*** nsExtensionManager::start - failure, catching exception so finalize window can close = " + e + "\n");
        }
        win.close();
        
        needsRestart = true;
      }

      if (aIsDirty) 
        needsRestart = this._finishOperations();
    }
    else {
      var win = this._showProgressWindow();
      try {    
        // Enter safe mode
        this._ensureDS();

        // Save the current theme (assumed to be the theme that styles the global
        // package) and re-select the default theme ("classic/1.0")
        if (!gPref.prefHasUserValue(PREF_EM_LAST_SELECTED_SKIN)) {
          gPref.setCharPref(PREF_EM_LAST_SELECTED_SKIN,
                            gPref.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN));
          if (gPref.prefHasUserValue(PREF_GENERAL_SKINS_SELECTEDSKIN))
            gPref.clearUserPref(PREF_GENERAL_SKINS_SELECTEDSKIN);
        }

        var items = this._ds.getItemList(null, nsIUpdateItem.TYPE_EXTENSION, {});
        for (var i = 0; i < items.length; ++i)
          this._finalizeEnableDisable(items[i].id, true);
          
        this._ds.safeMode = true;
        
        this._writeDefaults(true);
        try {
          this._writeDefaults(false);
        }
        catch (e) { }

        needsRestart = true;

        var wasInSafeModeFile = getFile(KEY_PROFILEDIR, [DIR_EXTENSIONS, FILE_WASINSAFEMODE]);
        if (!wasInSafeModeFile.exists())
          wasInSafeModeFile.create(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0644);
        else {
          // If the "Safe Mode" file already exists, then we are in the second launch of an
          // app launched with -safe-mode and so we don't want to provoke any further
          // restarts or re-create the file, just continue starting normally.
          needsRestart = false;
        }
      }
      catch (e) {
        dump("*** nsExtensionManager::start - (safe mode) failure, catching exception so finalize window can close = " + e + "\n");
      }
      win.close();
      
    }
    return needsRestart;
  },
  
  handleCommandLineArgs: function nsExtensionManager_handleCommandLineArgs (aCmdLine)
  {
    try {
      var globalExtension = aCmdLine.handleFlagWithParam("install-global-extension", false);
      if (globalExtension)
        this._checkForGlobalInstalls(globalExtension, nsIUpdateItem.TYPE_EXTENSION);
    }
    catch (e) {
      // The command line had the flag, but no param. XXXReport this error!
    }

    try {
      var globalTheme = aCmdLine.handleFlagWithParam("install-global-theme", false);
      if (globalTheme)
        this._checkForGlobalInstalls(globalTheme, nsIUpdateItem.TYPE_THEME);
    }
    catch (e) {
      // The command line had the flag, but no param. XXXReport this error!
    }

    if (aCmdLine.handleFlag("list-global-items", false))
      this._showGlobalItemList();

    try {
      var locked = aCmdLine.handleFlagWithParam("lock-item", false);
      if (locked) {
        this._ensureDS();
        this._ds.lockUnlockItem(locked, true);
      }
    }
    catch (e) {
      // The command line had the flag, but no param. XXXReport this error!
    }

    try {
      var unlocked = aCmdLine.handleFlagWithParam("unlock-item", false);
      if (unlocked) {
        this._ensureDS();
        this._ds.lockUnlockItem(unlocked, false);
      }
    }
    catch (e) {
      // The command line had the flag, but no param. XXXReport this error!
    }
    
    this._finishOperations();
  },
  
  register: function nsExtensionManager_register ()
  {
    if (!this._started) {
      // Loads the datasource and installs any pre-configured items.
      this._ds = new nsExtensionsDataSource();
      this._ds.loadExtensions(false);
      
      // Write bin/extensions/Extensions.rdf
      //       bin/extensions/installed-extensions-processed.txt
      (new nsInstalledExtensionReader(this)).read();

      // Write bin/components.ini
      var manifest = getFile(KEY_APPDIR, [FILE_COMPONENT_MANIFEST]);
      this._writeProfileFile(manifest, this._getComponentsDir, false);
    }
  },

  _cancelDownloads: function nsExtensionManager__cancelDownloads ()
  {
    for (var i = 0; i < this._transactions.length; ++i)
      gOS.notifyObservers(this._transactions[i], "xpinstall-progress", "cancel");
    gOS.removeObserver(this, "offline-requested");
    gOS.removeObserver(this, "quit-application-requested");

    this._removeAllDownloads();
  },

  _confirmCancelDownloads: function nsExtensionManager__confirmCancelDownloads(aCount, 
    aTitle, aCancelMessageMultiple, aCancelMessageSingle, aDontCancelButton)
  {
    var sbs = Components.classes["@mozilla.org/intl/stringbundle;1"]
                        .getService(Components.interfaces.nsIStringBundleService);
    var bundle = sbs.createBundle("chrome://mozapps/locale/downloads/downloads.properties");
    var title = bundle.GetStringFromName(aTitle);
    var message, quitButton;
    if (aCount > 1) {
      message = bundle.formatStringFromName(aCancelMessageMultiple, [aCount], 1);
      quitButton = bundle.formatStringFromName("cancelDownloadsOKTextMultiple", [aCount], 1);
    }
    else {
      message = bundle.GetStringFromName(aCancelMessageSingle);
      quitButton = bundle.GetStringFromName("cancelDownloadsOKText");
    }
    var dontQuitButton = bundle.GetStringFromName(aDontCancelButton);
    
    var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
                       .getService(Components.interfaces.nsIWindowMediator);
    var win = wm.getMostRecentWindow("Extension:Manager");
    const nsIPromptService = Components.interfaces.nsIPromptService;
    var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
                       .getService(nsIPromptService);
    var flags = (nsIPromptService.BUTTON_TITLE_IS_STRING * nsIPromptService.BUTTON_POS_0) +
                (nsIPromptService.BUTTON_TITLE_IS_STRING * nsIPromptService.BUTTON_POS_1);
    var rv = ps.confirmEx(win, title, message, flags, quitButton, dontQuitButton, null, null, { });
    return rv == 1;
  },
  
  // This function checks for and disables any "old-style" extensions 
  // from Firefox 0.8 and earlier created using the "chrome:extension=true" flag. 
  _disableObsoleteExtensions: function nsExtensionManager__disableObsoleteExtensions ()
  {
    if (!gPref.prefHasUserValue(PREF_EM_DISABLEDOBSOLETE) || !gPref.getBoolPref(PREF_EM_DISABLEDOBSOLETE)) {
      var win = this._showProgressWindow();
      try {
        var cr = Components.classes["@mozilla.org/chrome/chrome-registry;1"]
                          .getService(Components.interfaces.nsIXULChromeRegistry);
        var crDS = gRDF.GetDataSource("rdf:chrome");
        var disabled = false;
        var sources = crDS.GetSources(gRDF.GetResource(CHROME_NS("extension")), gRDF.GetLiteral("true"), true);
        while (sources.hasMoreElements()) {
          disabled = true;
          
          var source = sources.getNext().QueryInterface(Components.interfaces.nsIRDFResource);
          var name = crDS.GetTarget(source, gRDF.GetResource(CHROME_NS("name")), true);
          if (name) {
            name = name.QueryInterface(Components.interfaces.nsIRDFLiteral).Value;
            cr.setAllowOverlaysForPackage(name, false);
          }
        }

        // Re-select the default theme to prevent any incompatibilities with old-style
        // themes.
        cr.selectSkin(KEY_DEFAULT_THEME, true);
      }
      catch (e) {
        dump("*** nsExtensionManager::_disableObsoleteExtensions - failure, catching exception so finalize window can close\n");
      }
      win.close();
      
      if (disabled) {
        const nsIPromptService = Components.interfaces.nsIPromptService;
        var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
                           .getService(nsIPromptService);
        var sbs = Components.classes["@mozilla.org/intl/stringbundle;1"]
                            .getService(Components.interfaces.nsIStringBundleService);
        var bundle = sbs.createBundle("chrome://mozapps/locale/extensions/extensions.properties");
        var title = bundle.GetStringFromName("disabledObsoleteTitle");
        var message = bundle.GetStringFromName("disabledObsoleteMessage");
        ps.alert(null, title, message);
      }
            
      gPref.setBoolPref(PREF_EM_DISABLEDOBSOLETE, true);
    }
  },
  
  _checkForGlobalInstalls: function nsExtensionManager__checkForGlobalInstalls (aPath, aItemType)
  {

    // First see if the path supplied is a file path
    var file = Components.classes["@mozilla.org/file/local;1"]
                         .createInstance(Components.interfaces.nsILocalFile);
    try {
      file.initWithPath(aPath);
    }
    catch (e) {
      // Try appending the path to the current proc dir. 
      file = getDir(KEY_APPDIR, []);
      try {
        file.append(aPath);
      }
      catch (e) { /* can't handle this */ }
    }
    
    if (file.exists()) {
      if (aItemType & nsIUpdateItem.TYPE_EXTENSION)
        this.installExtension(file, nsIExtensionManager.FLAG_INSTALL_GLOBAL);
      else if (aItemType & nsIUpdateItem.TYPE_THEME)
        this.installTheme(file, nsIExtensionManager.FLAG_INSTALL_GLOBAL);
    }
    else
      dump("Invalid XPI/JAR Path: " + aPath + "\n");
  },
  
  _showGlobalItemList: function nsExtensionManager__showGlobalItemList ()
  {
    this._ensureDS();
    
    var sbs = Components.classes["@mozilla.org/intl/stringbundle;1"]
                        .getService(Components.interfaces.nsIStringBundleService);
    var bundle = sbs.createBundle("chrome://mozapps/locale/extensions/extensions.properties");

    dump(bundle.GetStringFromName("globalItemList"));
    dump(bundle.GetStringFromName("globalItemListExtensions"));
    var items = this.getItemList(null, nsIUpdateItem.TYPE_EXTENSION, {});
    for (var i = 0; i < items.length; ++i)
      dump(" " + items[i].id + "   " + items[i].name + " " + items[i].version + "\n");
    dump(bundle.GetStringFromName("globalItemListThemes"));
    items = this.getItemList(null, nsIUpdateItem.TYPE_THEME, {});
    for (var i = 0; i < items.length; ++i)
      dump(" " + items[i].id + "   " + items[i].name + " " + items[i].version + "\n");
      
    dump("\n\n");
  },
  
  _finishOperations: function nsExtensionManager__finishOperations ()
  {
    var win = this._showProgressWindow();
  
    try {
      // An existing autoreg file is an indication that something major has 
      // happened to the extensions datasource (install/uninstall/enable/disable)
      // and as such we must load it now and see what needs to happen.
      this._ensureDS();
      
      // Look for items that need to be installed
      var items = this._ds.getItemsWithFlagSet("toBeInstalled");
      for (var i = 0; i < items.length; ++i)
        this._finalizeInstall(items[i]);

      // If there were any install operations, we need to restart (again!) after 
      // the component files have been properly installed are registered...
      var needsRestart = items.length > 0;
      
      // Look for extensions that need to be enabled
      items = this._ds.getItemsWithFlagSet("toBeEnabled");
      for (var i = 0; i < items.length; ++i)
        this._finalizeEnableDisable(items[i], false);
      
      // Look for extensions that need to be disabled
      items = this._ds.getItemsWithFlagSet("toBeDisabled");
      for (var i = 0; i < items.length; ++i)
        this._finalizeEnableDisable(items[i], true);
      
      // Look for extensions that need to be removed. This MUST be done after
      // the install operations since extensions to be installed may have to be
      // uninstalled if there are errors during the installation process!
      items = this._ds.getItemsWithFlagSet("toBeUninstalled");
      for (var i = 0; i < items.length; ++i)
        this._finalizeUninstall(items[i]);
        
      // Clean up any helper objects
      delete this._extInstaller;
      delete this._extUninstaller;
      delete this._extEnabler;
      
      this._updateManifests();
      // If no additional restart is required, it implies that there are
      // no new components that need registering so we can inform the app
      // not to do any extra startup checking next time round.    
      this._setComponentsListChanged(needsRestart);
    }
    catch (e) {
      dump("*** nsExtensionManager::_finishOperations - failure, catching exception so finalize window can close " + e +"\n");
    }
    win.close();
    
    return needsRestart;
  },
  
  // XXXben - this is actually a cheap stunt to load all the chrome registry 
  //          services required to register/unregister packages... the synchronous
  //          nature of this code ensures the window will never actually appear
  //          on screen. 
  _showProgressWindow: function nsExtensionManager__showProgressWindow ()
  {
    var ww = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
                       .getService(Components.interfaces.nsIWindowWatcher);
    return ww.openWindow(null, "chrome://mozapps/content/extensions/finalize.xul", 
                         "", "chrome,centerscreen,dialog", null);
  },
  
  _loadDefaults: function nsExtensionManager__loadDefaults ()
  {
    // Load default preferences files for all extensions
    var defaultsManifest = getFile(KEY_PROFILEDIR, 
                                   [DIR_EXTENSIONS, FILE_DEFAULTS]);
    if (defaultsManifest.exists()) {
      var fis = Components.classes["@mozilla.org/network/file-input-stream;1"]
                          .createInstance(Components.interfaces.nsIFileInputStream);
      fis.init(defaultsManifest, -1, -1, false);
      var lis = fis.QueryInterface(Components.interfaces.nsILineInputStream);
      var line = { value: "" };
      var more = false;
      do {
        more = lis.readLine(line);
        var lf = Components.classes["@mozilla.org/file/local;1"]
                            .createInstance(Components.interfaces.nsILocalFile);
        var path = line.value;
        if (path) {
          lf.initWithPath(path);
          
          if (lf.exists())
            gPref.readUserPrefs(lf);
        }
      }
      while (more);
      fis.close();
    }
  },
  
  ensurePreConfiguredItem: function nsExtensionManager_ensurePreConfiguredItem (aItemID, aItemType, aManifest)
  {
    this._ds.insertForthcomingItem(aItemID, aItemType, false);
    var metadataDS = getInstallManifest(aManifest);
    this._ds.addItemMetadata(aItemID, aItemType, metadataDS, false);
  },
  
  checkForMismatches: function nsExtensionManager_checkForMismatches () 
  {
    var needsRestart = false;
        
    this._disableObsoleteExtensions();

    // Check to see if the version of the application that is being started
    // now is the same one that was started last time. 
    var currAppVersion = gPref.getCharPref(PREF_EM_APP_EXTENSIONS_VERSION);
    try {
      var lastAppVersion = gPref.getCharPref(PREF_EM_LAST_APP_VERSION);
    }
    catch (e) {}
    if (currAppVersion != lastAppVersion) {
      // Version mismatch, we're have to load the extensions datasource
      // and do version checking. Time hit here doesn't matter since this 
      // doesn't happen all that often.
      this._ensureDS();
      var currAppID = gApp.ID;
      var items = this._ds.getIncompatibleItemList(currAppID, currAppVersion,
                                                   nsIUpdateItem.TYPE_ADDON);
      if (items.length > 0) {
        for (var i = 0; i < items.length; ++i) {
          // Now disable the extension so it won't hurt anything. 
          var itemType = getItemType(this._ds._getResourceForItem(items[i].id).Value);
          if (itemType != -1 && itemType & nsIUpdateItem.TYPE_EXTENSION)
            this.disableExtension(items[i].id);
          else if (itemType & nsIUpdateItem.TYPE_THEME) {
            if (gPref.prefHasUserValue(PREF_GENERAL_SKINS_SELECTEDSKIN))
              gPref.clearUserPref(PREF_GENERAL_SKINS_SELECTEDSKIN);
            this._ds.disableTheme(items[i].id);
          }
        }
        var updates = Components.classes["@mozilla.org/updates/update-service;1"]
                                .getService(Components.interfaces.nsIUpdateService);
        updates.checkForUpdates(items, items.length, nsIUpdateItem.TYPE_ADDON, 
                                nsIUpdateService.SOURCE_EVENT_MISMATCH,
                                null);
        
        needsRestart = true;
      }
    }
    
    // Somehow the component list went away, and for that reason the new one
    // generated by this function is going to result in a different compreg.
    // We must force a restart.
    var componentList = getFile(KEY_PROFILEDIR, [FILE_COMPONENT_MANIFEST]);
    if (!componentList.exists())
      needsRestart = true;
    
    // Now update the last app version so we don't do this checking 
    // again. 
    gPref.setCharPref(PREF_EM_LAST_APP_VERSION, currAppVersion);

    // XXXben - I am not entirely sure this is needed, since components and 
    // defaults manifests are written by the disabling function. Not going to
    // rock the boat now however. 
    this._updateManifests();
    
    return needsRestart;
  },
  
  get inSafeMode() 
  {
    return this._ds.safeMode;
  },
  
  _updateManifests: function nsExtensionManager__updateManifests ()
  {
    // Update the components manifests with paths for compatible, enabled, 
    // extensions.
    try {
      // Wrap this in try..catch so that if the account is restricted we don't
      // completely fail here for lack of permissions to write to the bin
      // dir (and cause apprunner to go into a restart loop). 
      //
      // This means that making changes to install-dir extensions only possible
      // for people with write access to bin dir (i.e. uninstall, disable, 
      // enable)
      this._writeComponentManifest(false);
      this._writeDefaults(false);
    }
    catch (e) { 
      dump("*** ExtensionManager:_updateManifests: no access privileges to application directory, skipping.\n"); 
    };
    this._writeComponentManifest(true);
    this._writeDefaults(true);
  },
  
  // XXXben write to temporary file then move to final when done.
  _writeProfileFile: function nsExtensionManager__writeProfileFile (aFile, aGetDirFunc, aIsProfile)
  {
    // When an operation is performed that requires a component re-registration
    // (extension enabled/disabled, installed, uninstalled), we must write the
    // set of registry-relative paths of components to register to an .autoreg 
    // file which lives in the profile folder. 
    //
    // To do this we must enumerate all installed extensions and write data 
    // about all valid items to the file. 
    this._ensureDS();
    
    var fos = Components.classes["@mozilla.org/network/file-output-stream;1"]
                        .createInstance(Components.interfaces.nsIFileOutputStream);
    const MODE_WRONLY   = 0x02;
    const MODE_CREATE   = 0x08;
    const MODE_TRUNCATE = 0x20;
    fos.init(aFile, MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE, 0644, 0);

    var extensions = this.getItemList(null, nsIUpdateItem.TYPE_EXTENSION, { });
    var validExtensions = [];
    for (var i = 0; i < extensions.length; ++i) {
      var extension = extensions[i];
    
      // An extension entry is valid only if it is not disabled, not about to 
      // be disabled, and not about to be uninstalled.
      var toBeDisabled = this._ds.getItemProperty(extension.id, "toBeDisabled");
      var toBeUninstalled = this._ds.getItemProperty(extension.id, "toBeUninstalled");
      var toBeInstalled = this._ds.getItemProperty(extension.id, "toBeInstalled");
      var disabled = this._ds.getItemProperty(extension.id, "disabled");
      if (toBeDisabled == "true" || toBeUninstalled == "true" || 
          disabled == "true" || toBeInstalled == "true")
        continue;
      
      var isProfile = this._ds.isProfileItem(extension.id);
      var sourceDir = aGetDirFunc(isProfile, extension.id);
      if (sourceDir.exists() && (aIsProfile == isProfile))
        validExtensions.push({ sourceDir: sourceDir, isProfile: isProfile });
    }
    
    var lines = ["[Extra Files]\r\n",
                 "Count=" + validExtensions.length + "\r\n"];
    for (i = 0; i < lines.length; ++i)
      fos.write(lines[i], lines[i].length);
      
    for (i = 0; i < validExtensions.length; ++i) {
      var e = validExtensions[i];
      var relativeDir = getDir(e.isProfile ? KEY_PROFILEDIR : KEY_APPDIR, []);
      var lf = e.sourceDir.QueryInterface(Components.interfaces.nsILocalFile);
      var relDesc = lf.getRelativeDescriptor(relativeDir);
      var line = "File" + i + "=" + relDesc + "\r\n";
      fos.write(line, line.length);
    }
    fos.close();
  },
  
  _getComponentsDir: function nsExtensionManager__getComponentsDir (aIsProfile, aExtensionID)
  {
    return getDirNoCreate(getDirKey(aIsProfile), 
                          [DIR_EXTENSIONS, aExtensionID, DIR_COMPONENTS]);
  },
 
  _getPreferencesDir: function nsExtensionManager__getPreferencesDir (aIsProfile, aExtensionID)
  {
    return getDirNoCreate(getDirKey(aIsProfile), 
                          [DIR_EXTENSIONS, aExtensionID, 
                           DIR_DEFAULTS, DIR_DEFAULTS_PREFS]);
  },

  _writeComponentManifest: function nsExtensionManager__writeComponentManifest (aIsProfile)
  {
    var manifest = aIsProfile ? getFile(KEY_PROFILEDIR, [FILE_COMPONENT_MANIFEST]) : 
                                getFile(KEY_APPDIR, [FILE_COMPONENT_MANIFEST]);
    this._writeProfileFile(manifest, this._getComponentsDir, aIsProfile);

    // Now refresh the compatibility manifest.
    this._setComponentsListChanged(true);
  },
  
  _setComponentsListChanged: function nsExtensionManager__setComponentsListChanged (aComponentListUpdated)
  {
    // This code assumes that we are the only one responsible for creating this
    // file.  That's OK for now, but in the future we may need to replace this
    // with code that manages a reference count or something jazzy like that.

    var autoregFile = getFile(KEY_PROFILEDIR, [FILE_AUTOREG]);
    try {
      if (aComponentListUpdated)
        autoregFile.create(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0600);
      else
        autoregFile.remove(false);
    }
    catch (e) {}
  },
  
  _writeDefaults: function nsExtensionManager__writeDefaults (aIsProfile)
  {
    var manifest = aIsProfile ? getFile(KEY_PROFILEDIR, [FILE_DEFAULTS]) : 
                                getFile(KEY_APPDIR, [FILE_DEFAULTS]);
    this._writeProfileFile(manifest, this._getPreferencesDir, aIsProfile);
  },
  
  _cleanDirs: function nsExtensionManager__cleanDirs ()
  {
    var keys = [KEY_PROFILEDIR, KEY_APPDIR];
    for (var i = 0; i < keys.length; ++i) {
      var extensions = getDir(keys[i], [DIR_EXTENSIONS]);
      var entries = extensions.directoryEntries;
      while (entries.hasMoreElements()) {
        var entry = entries.getNext().QueryInterface(Components.interfaces.nsIFile);
        if (entry.isDirectory() && !entry.directoryEntries.hasMoreElements()) {
          try {
            entry.remove(false);
          }
          catch (e) { }
        }
      }
    }
  },
  
  /////////////////////////////////////////////////////////////////////////////  
  // nsIExtensionManager
  installExtension: function nsExtensionManager_installExtension (aXPIFile, aFlags)
  {
    // Since we're installing a "new type" extension, we assume a file layout
    // within the XPI like so:
    // foo.xpi/
    //         extension.rdf
    //         chrome/
    //         components/ 
    //         defaults/
    //                  prefs/
    var installProfile = aFlags & nsIExtensionManager.FLAG_INSTALL_PROFILE;

    var tempDir = getDir(getDirKey(installProfile), [DIR_EXTENSIONS, DIR_TEMP]);
    var fileName = getRandomFileName("temp", "xpi");
    aXPIFile.copyTo(tempDir, fileName);
    var xpiFile = tempDir.clone();
    xpiFile.append(fileName);

    // if the source file was read-only, fix permissions
    if (!xpiFile.isWritable()) {
      xpiFile.permissions = 0644;
    }

    var zipReader = Components.classes["@mozilla.org/libjar/zip-reader;1"]
                              .createInstance(Components.interfaces.nsIZipReader);
    zipReader.init(xpiFile);
    zipReader.open();
    
    var tempManifest = getFile(getDirKey(installProfile),
                               [DIR_EXTENSIONS, DIR_TEMP, getRandomFileName("install", "rdf")]);
    if (!tempManifest.exists())
      tempManifest.create(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0644);
    zipReader.extract(FILE_INSTALL_MANIFEST, tempManifest);
    
    var extensionID = this.installExtensionInternal(xpiFile, tempManifest, installProfile);
    switch (extensionID) {
    case ERROR_EXTENSION_IS_THEME:
      this.installTheme(aXPIFile, aFlags);
      break;
    case ERROR_INVALID_VERSION:
    case ERROR_PHONED_HOME:
      break;
    default:
      // Then we stage the extension's XPI into a temporary directory so we 
      // can extract them after the next restart. 
      this._stageExtensionXPI(zipReader, extensionID, installProfile);

      this._writeComponentManifest(installProfile);
    }
    
    zipReader.close();
    tempManifest.remove(false);
    
    if (extensionID != ERROR_PHONED_HOME)
      xpiFile.remove(false);
  },
  
  installExtensionInternal: function nsExtensionManager_installExtensionInternal (aXPIFile, aManifest, aIsProfile)
  {
    var ds = getInstallManifest(aManifest);
    if (!ds) return;
    
    // XXXben - this is a hack until we properly fix xpinstall to be able to install
    //          different chrome types from trusted script. At the moment, when we
    //          call initManagerFromChrome, we can only install extensions, since
    //          the code path that installs themes is not utilized. To minimize the
    //          level of changes happening at the lower level in xpinstall at this
    //          point I am inserting this hack which checks for a theme-only property
    //          in the install manifest.
    var manifestRoot = gRDF.GetResource("urn:mozilla:install-manifest");
    var internalName = gRDF.GetResource(EM_NS("internalName"));
    if (stringData(ds.GetTarget(manifestRoot, internalName, true)) != "--")
      return ERROR_EXTENSION_IS_THEME;
    
    // We do a basic version check first just to make sure we somehow weren't 
    // tricked into installing an incompatible extension...
    this._ensureDS();
    var extensionID = this.canInstallItem(ds);
    // |extensionID| must be a GUID string, not a number - a number means failure.
    if (isNaN(parseInt(extensionID)))
      this._configureForthcomingItem(ds, extensionID, aIsProfile);
    else if (extensionID == 0) {
      var io = new this.IncompatibleObserver(this);
      var isChecking = io.checkForUpdates(ds, nsIUpdateItem.TYPE_EXTENSION,
                                          aXPIFile, aIsProfile);
      if (!isChecking)
        showIncompatibleError(ds);
      else {
        extensionID = ERROR_PHONED_HOME; // caller uses this to distinguish 
                                         // phone-home attempt.
      }
    }
    
    return extensionID;
  },
  
  IncompatibleObserver: function nsExtensionManager_IncompatibleObserver (aEM) 
  {
    this._item = null;
    this._em = aEM;
    this._ds = null;
    this._xpi = null;
    this._extensionID = 0;
    this._isProfile = true;
    
    this.checkForUpdates = function nsExtensionManager__iOcheckForUpdates (aDataSource, aType, 
                                                                           aXPIFile, aIsProfile)
    {
      // Construct a nsIUpdateItem for this extension...
      var item = this._em._getItemForIncompatibleID(aDataSource, aType);
      if (item) {
        this._item      = item;
        this._ds        = aDataSource;
        this._xpi       = aXPIFile;
        this._isProfile = true;
        
        gOS.addObserver(this, "Update:Extension:Started", false);
        gOS.addObserver(this, "Update:Extension:Item-Ended", false);
        gOS.addObserver(this, "Update:Extension:Item-Error", false);
        gOS.addObserver(this, "Update:Extension:Ended", false);

        this._em.update([item], 1, true);
        
        return true;
      }
      return false;
    }
    
    this.observe = function nsExtensionManager__iOobserve (aSubject, aTopic, aData)
    {
      switch (aTopic) {
      case "Update:Extension:Started":
        break;
      case "Update:Extension:Item-Ended":
        if (aSubject) {
          var item = aSubject.QueryInterface(Components.interfaces.nsIUpdateItem);
          this._em._ds.setTargetApplicationInfo(item.id, 
                                                item.minAppVersion,
                                                item.maxAppVersion, 
                                                this._ds, 
                                                this._item.type);
          this._extensionID = this._em.canInstallItem(this._ds);
        }
        break;
      case "Update:Extension:Item-Error":
        break;
      case "Update:Extension:Ended":
        gOS.removeObserver(this, "Update:Extension:Started");
        gOS.removeObserver(this, "Update:Extension:Item-Ended");
        gOS.removeObserver(this, "Update:Extension:Item-Error");
        gOS.removeObserver(this, "Update:Extension:Ended");
        
        if (isNaN(this._extensionID)) {
          var zipReader = Components.classes["@mozilla.org/libjar/zip-reader;1"]
                                    .createInstance(Components.interfaces.nsIZipReader);
          zipReader.init(this._xpi);
          zipReader.open();

          // Add the item after all
          this._em._configureForthcomingItem(this._ds, this._extensionID, 
                                             this._isProfile);
          this._em._stageExtensionXPI(zipReader, this._extensionID, this._isProfile);
          this._em._writeComponentManifest(this._isProfile);
          
          zipReader.close();
        }
        else 
          showIncompatibleError(this._ds);
        
        // Now really delete the temporary XPI file
        this._xpi.remove(false);
        break;
      }
    }
  },
  
  _configureForthcomingItem: function nsExtensionManager__configureForthcomingItem (aDataSource, 
                                                                                    aExtensionID, 
                                                                                    aIsProfile)
  {
    // Clear any "disabled" flags that may have been set by the mismatch 
    // checking code at startup.
    var props = { toBeDisabled  : null,
                  disabled      : null,
                  toBeInstalled : this._ds._emL("true"),
                  name          : this.getManifestProperty(aDataSource, "name"),
                  version       : this.getManifestProperty(aDataSource, "version") };
    for (var p in props) {
      this._ds.setItemProperty(aExtensionID, this._ds._emR(p),
                               props[p], aIsProfile,
                               nsIUpdateItem.TYPE_EXTENSION);
    }

    // Insert it into the child list NOW rather than later because:
    // - extensions installed using the command line need to be a member
    //   of a container during the install phase for the code to be able
    //   to identify profile vs. global
    // - extensions installed through the UI should show some kind of
    //   feedback to indicate their presence is forthcoming (i.e. they
    //   will be available after a restart).
    this._ds.insertForthcomingItem(aExtensionID, nsIUpdateItem.TYPE_EXTENSION, 
                                   aIsProfile);
  },
  
  _getItemForIncompatibleID: function nsExtensionManager__getItemForID (aDataSource, aType)
  {
    var newItem = null;
    var id, version, targetAppInfo, name, updateURL;
    var manifestRoot = gRDF.GetResource("urn:mozilla:install-manifest");
    try {
      function getProperty (aDataSource, aSourceResource, aProperty)
      {
        var rv;
        try {
          var property = gRDF.GetResource(EM_NS(aProperty));
          rv = stringData(aDataSource.GetTarget(aSourceResource, property, true));
          if (rv == "--")
            throw Components.results.NS_ERROR_FAILURE;
        }
        catch (e) { }
        return rv;
      }
      
      var root = gRDF.GetResource("urn:mozilla:install-manifest");
      id            = getProperty(aDataSource, root, "id");
      version       = getProperty(aDataSource, root, "version");
      targetAppInfo = this._ds.getTargetApplicationInfo(id, aDataSource, aType);
      name          = getProperty(aDataSource, root, "name");
      updateURL     = getProperty(aDataSource, root, "updateURL");
      if (updateURL == "--")
        updateURL = "";
      
      newItem = Components.classes["@mozilla.org/updates/item;1"]
                          .createInstance(Components.interfaces.nsIUpdateItem);
      newItem.init(id, version, targetAppInfo.minVersion, 
                   targetAppInfo.maxVersion,
                   name, -1, "", "", updateURL, aType);
    }
    catch (e) {
      return null;
    }
    return newItem;
  },
  
  canInstallItem: function nsExtensionManager_canInstallItem (aDataSource)
  {
    var manifestRoot = gRDF.GetResource("urn:mozilla:install-manifest");
    // First make sure the item has a valid "version" property. 
    var version = gRDF.GetResource(EM_NS("version"));
    var versionLiteral = stringData(aDataSource.GetTarget(manifestRoot, version, true));
    if (!getVersionChecker().isValidVersion(versionLiteral)) {
      var name = gRDF.GetResource(EM_NS("name"));
      var nameLiteral = stringData(aDataSource.GetTarget(manifestRoot, name, true));
      showInvalidVersionError(nameLiteral, versionLiteral);
      return ERROR_INVALID_VERSION;
    }
        
    // Check the target application range specified by the extension metadata.
    if (this._ds.isCompatible(aDataSource, manifestRoot)) {
      var id = gRDF.GetResource(EM_NS("id"));
      var idLiteral = aDataSource.GetTarget(manifestRoot, id, true);
      return idLiteral.QueryInterface(Components.interfaces.nsIRDFLiteral).Value;
    }
    return 0;
  },
  
  getManifestProperty: function nsExtensionManager_getManifestProperty (aDataSource, aProperty)
  {
    var manifestRoot = gRDF.GetResource("urn:mozilla:install-manifest");
    var arc = gRDF.GetResource(EM_NS(aProperty));
    return aDataSource.GetTarget(manifestRoot, arc, true);
  },
  
  _stageExtensionXPI: function nsExtensionManager__stageExtensionXPI (aZipReader, aExtensionID, aInstallProfile)
  {
    // Get the staging dir
    var dir = getDir(getDirKey(aInstallProfile),
                     [DIR_EXTENSIONS, DIR_TEMP, aExtensionID]);
    var extensionFileName = aExtensionID + ".xpi";
    var extensionFile = dir.clone();
    extensionFile.append(extensionFileName);
    if (extensionFile.exists())
      extensionFile.remove(false);
    aZipReader.file.copyTo(dir, extensionFileName);

    // if the source file was readonly, fix the permissions
    if (!extensionFile.isWritable()) {
      extensionFile.permissions = 0644;
    }
  },
  
  // This function is called on the next startup 
  _finalizeInstall: function nsExtensionManager__finalizeInstall (aExtensionID)
  {
    var isProfile = this._ds.isProfileItem(aExtensionID);
    if (aExtensionID == 0 || aExtensionID == -1) {
      this._ds.removeCorruptItem(aExtensionID, 
                                 nsIUpdateItem.TYPE_EXTENSION, 
                                 isProfile);
      return;
    }
    
    if (!this._extInstaller)
      this._extInstaller = new nsExtensionInstaller(this._ds);
      
    this._extInstaller.install(aExtensionID, isProfile);
    
    // Update the Components Manifest
    this._writeComponentManifest(isProfile);
    
    // Update the Defaults Manifest
    this._writeDefaults(isProfile);
  },
  
  _finalizeEnableDisable: function nsExtensionManager__finalizeEnableDisable (aExtensionID, aDisable)
  {
    if (!this._extEnabler)
      this._extEnabler = new nsExtensionEnabler(this._ds);
      
    var isProfile = this._ds.isProfileItem(aExtensionID);
    this._extEnabler.enable(aExtensionID, isProfile, aDisable);

    // clear temporary flags
    this._ds.setItemProperty(aExtensionID, 
                             this._ds._emR("toBeEnabled"),
                             null, isProfile,
                             nsIUpdateItem.TYPE_EXTENSION);
    this._ds.setItemProperty(aExtensionID, 
                             this._ds._emR("toBeDisabled"),
                             null, isProfile,
                             nsIUpdateItem.TYPE_EXTENSION);
  },
  
  _finalizeUninstall: function nsExtensionManager__finalizeUninstall (aExtensionID)
  {
    if (!this._extUninstaller)
      this._extUninstaller = new nsExtensionUninstaller(this._ds);
    var isProfile = this._ds.isProfileItem(aExtensionID);
    this._extUninstaller.uninstall(aExtensionID, isProfile);

    // Clean the extension resource
    this._ds.removeItemMetadata(aExtensionID, nsIUpdateItem.TYPE_EXTENSION);
    
    // Do this LAST since inferences are made about an item based on
    // what container it's in.
    this._ds.removeItemFromContainer(aExtensionID, 
                                     nsIUpdateItem.TYPE_EXTENSION,
                                     isProfile);
  },
      
  uninstallExtension: function nsExtensionManager_uninstallExtension (aExtensionID)
  {
    if (!this._ds.isDownloadItem(aExtensionID)) {
      this._ds.uninstallExtension(aExtensionID);

      var isProfile = this._ds.isProfileItem(aExtensionID);

      // Update the Components Manifest
      this._writeComponentManifest(isProfile);

      // Update the Defaults Manifest
      this._writeDefaults(isProfile);
    }
    else {
      // Bad download entry - uri is url, e.g. "http://www.foo.com/test.xpi"
      // ... just remove it from the list. 
      this._ds.removeCorruptDLItem(aExtensionID, nsIUpdateItem.TYPE_EXTENSION);
    }
  },
  
  enableExtension: function nsExtensionManager_enableExtension (aExtensionID)
  {
    this._ds.enableExtension(aExtensionID);

    var isProfile = this._ds.isProfileItem(aExtensionID);

    // Update the Components Manifest
    this._writeComponentManifest(isProfile);

    // Update the Defaults Manifest
    this._writeDefaults(isProfile);
  },
  
  disableExtension: function nsExtensionManager_disableExtension (aExtensionID)
  {
    this._ds.disableExtension(aExtensionID);

    var isProfile = this._ds.isProfileItem(aExtensionID);

    // Update the Components Manifest
    this._writeComponentManifest(isProfile);

    // Update the Defaults Manifest
    this._writeDefaults(isProfile);
  },
  
  enableTheme: function nsExtensionsDataSource_enableTheme (aThemeID)
  {
    this._ds.enableTheme(aThemeID);
  },
    
  disableTheme: function nsExtensionsDataSource_disableTheme (aThemeID)
  {
    this._ds.disableTheme(aThemeID);
  },
  
  update: function nsExtensionManager_update (aItems, aItemCount, aVersionUpdateOnly)
  {
    var appID = gApp.ID;
    var appVersion = gPref.getCharPref(PREF_EM_APP_EXTENSIONS_VERSION);

    if (aItems.length == 0) {
      var addonType = nsIUpdateItem.TYPE_ADDON;
      aItems = this.getItemList(null, addonType, { });
    }
    var updater = new nsExtensionItemUpdater(appID, appVersion, this);
    updater.checkForUpdates(aItems, aItems.length, aVersionUpdateOnly);
  },
  
  getItemList: function nsExtensionManager_getItemList (aItemID, aType, aCountRef)
  {
    this._ensureDS();
    return this._ds.getItemList(aItemID, aType, aCountRef);
  },    

  /////////////////////////////////////////////////////////////////////////////  
  // Themes
  installTheme: function nsExtensionManager_installTheme (aJARFile, aFlags)
  {
    this._ensureDS();
    
    var isProfile = aFlags & nsIExtensionManager.FLAG_INSTALL_PROFILE;
    var installer = new nsThemeInstaller(this._ds, this);
    installer.install(aJARFile, isProfile);
    // XPInstall selects the theme, if necessary.
  },
  
  uninstallTheme: function nsExtensionManager_uninstallTheme (aThemeID)
  {
    if (!this._ds.isDownloadItem(aThemeID)) {
      this._ensureDS();
      this._ds.uninstallTheme(aThemeID);
    }
    else {
      // Bad download entry - uri is url, e.g. "http://www.foo.com/test.jar"
      // ... just remove it from the list. 
      this._ds.removeCorruptDLItem(aThemeID, nsIUpdateItem.TYPE_THEME);
    }
  },
  
  moveTop: function nsExtensionManager_moveTop (aItemID)
  {
    this._ds.moveTop(aItemID);
  },
  
  moveUp: function nsExtensionManager_moveUp (aItemID)
  {
    this._ds.moveUp(aItemID);
  },
  
  moveDown: function nsExtensionManager_moveDown (aItemID)
  {
    this._ds.moveDown(aItemID);
  },

  get datasource()
  {
    this._ensureDS();
    return this._ds;
  },
  
  /////////////////////////////////////////////////////////////////////////////    
  // Downloads
  _transactions: [],
  _downloadCount: 0,
  addDownloads: function nsExtensionManager_addDownloads (aItems, aItemCount)
  {
    this._downloadCount += aItemCount;
    
    var txn = new nsItemDownloadTransaction(this);
    for (var i = 0; i < aItemCount; ++i) {
      var currItem = aItems[i];
      var txnID = Math.round(Math.random() * 100);
      txn.addDownload(currItem.name, currItem.xpiURL, currItem.iconURL, 
                      currItem.type, txnID);
      this._transactions.push(txn);
    }

    // Kick off the download process for this transaction
    gOS.addObserver(this, "offline-requested", false);
    gOS.addObserver(this, "quit-application-requested", false);
    gOS.notifyObservers(txn, "xpinstall-progress", "open");  
  },
  
  removeDownload: function nsExtensionManager_removeDownload (aURL, aType)
  {
    for (var i = 0; i < this._transactions.length; ++i) {
      if (this._transactions[i].containsURL(aURL)) {
        this._transactions[i].removeDownload(aURL, aType);
        return;
      }
    } 
  },
  
  _removeAllDownloads: function nsExtensionManager__removeAllDownloads ()
  {
    for (var i = 0; i < this._transactions.length; ++i)
      this._transactions[i].removeAllDownloads();
  },
  
  // The nsIXPIProgressDialog implementation in the download transaction object
  // forwards notifications through these methods which we then pass on to any
  // front end objects implementing nsIExtensionDownloadProgressListener that 
  // are listening. We maintain the master state of download operations HERE, 
  // not in the front end, because if the user closes the extension or theme 
  // managers during the downloads we need to maintain state and not terminate
  // the download/install process. 
  onStateChange: function nsExtensionManager_onStateChange (aTransaction, aURL, aState, aValue)
  {
    if (!(aURL in this._progressData)) 
      this._progressData[aURL] = { };
    this._progressData[aURL].state = aState;
    
    for (var i = 0; i < this._downloadObservers.length; ++i)
      this._downloadObservers[i].onStateChange(aURL, aState, aValue);

    const nsIXPIProgressDialog = Components.interfaces.nsIXPIProgressDialog;
    switch (aState) {
    case nsIXPIProgressDialog.INSTALL_DONE:
      --this._downloadCount;
      break;
    case nsIXPIProgressDialog.DIALOG_CLOSE:
      for (var i = 0; i < this._transactions.length; ++i) {
        if (this._transactions[i].id == aTransaction.id) {
          this._transactions.splice(i, 1);
          delete aTransaction;
          break;
        }
      }
      break;
    }
  },
  
  _progressData: { },
  onProgress: function nsExtensionManager_onProgress (aURL, aValue, aMaxValue)
  {
    for (var i = 0; i < this._downloadObservers.length; ++i)
      this._downloadObservers[i].onProgress(aURL, aValue, aMaxValue);
    
    if (!(aURL in this._progressData)) 
      this._progressData[aURL] = { };
    this._progressData[aURL].progress = Math.round((aValue / aMaxValue) * 100);
  },

  _downloadObservers: [],
  addDownloadObserver: function nsExtensionManager_addDownloadObserver (aXPIProgressDialog)
  {
    for (var i = 0; i < this._downloadObservers.length; ++i) {
      if (this._downloadObservers[i] == aXPIProgressDialog)
        return i;
    }
    this._downloadObservers.push(aXPIProgressDialog);
    return this._downloadObservers.length - 1;
  },
  
  removeDownloadObserverAt: function nsExtensionManager_removeDownloadObserverAt (aIndex)
  {
    this._downloadObservers.splice(aIndex, 1);
    if (this._downloadCount != 0)
      this._ds.flushProgressInfo(this._progressData);
  },

  //
  _ds: null,

  /////////////////////////////////////////////////////////////////////////////    
  // Other
  
  // This should NOT be called until after the window is shown! 
  _ensureDS: function nsExtensionManager__ensureDS ()
  {
    if (!this._ds) {
      dump("*** loading the extensions datasource\n");
      this._ds = new nsExtensionsDataSource();
      if (this._ds) {
        this._ds.loadExtensions(false);
        this._ds.loadExtensions(true);
      }
      
      // Ensure any pre-configured items are initialized.
      (new nsInstalledExtensionReader(this)).read();
    }
  },

  /////////////////////////////////////////////////////////////////////////////
  // nsIClassInfo
  getInterfaces: function nsExtensionManager_getInterfaces (aCount)
  {
    var interfaces = [Components.interfaces.nsIExtensionManager,
                      Components.interfaces.nsIXPIProgressDialog,
                      Components.interfaces.nsIObserver];
    aCount.value = interfaces.length;
    return interfaces;
  },
  
  getHelperForLanguage: function nsExtensionManager_getHelperForLanguage (aLanguage)
  {
    return null;
  },
  
  get contractID() 
  {
    return "@mozilla.org/extensions/manager;1";
  },
  
  get classDescription()
  {
    return "Extension Manager";
  },
  
  get classID() 
  {
    return Components.ID("{8A115FAA-7DCB-4e8f-979B-5F53472F51CF}");
  },
  
  get implementationLanguage()
  {
    return Components.interfaces.nsIProgrammingLanguage.JAVASCRIPT;
  },
  
  get flags()
  {
    return Components.interfaces.nsIClassInfo.SINGLETON;
  },

  /////////////////////////////////////////////////////////////////////////////
  // nsISupports
  QueryInterface: function nsExtensionManager_QueryInterface (aIID) 
  {
    if (!aIID.equals(Components.interfaces.nsIExtensionManager) &&
        !aIID.equals(Components.interfaces.nsIObserver) &&
        !aIID.equals(Components.interfaces.nsISupports))
      throw Components.results.NS_ERROR_NO_INTERFACE;
    return this;
  }
};

///////////////////////////////////////////////////////////////////////////////
//
// nsItemDownloadTransaction
//
//   This object implements nsIXPIProgressDialog and represents a collection of
//   XPI/JAR download and install operations. There is one 
//   nsItemDownloadTransaction per back-end XPInstallManager object. We maintain
//   a collection of separate transaction objects because it's possible to have
//   multiple separate XPInstall download/install operations going on 
//   simultaneously, each with its own XPInstallManager instance. For instance
//   you could start downloading two extensions and then download a theme. Each
//   of these operations would open the appropriate FE and have to be able to
//   track each operation independently.
//
function nsItemDownloadTransaction(aManager)
{
  this._manager = aManager;
  this._downloads = [];
}

nsItemDownloadTransaction.prototype = {
  _manager    : null,
  _downloads  : [],
  id          : -1,
  
  addDownload: function nsItemDownloadTransaction_addDownload (aName, aURL, aIconURL, aItemType, aID)
  {
    this._downloads.push({ url: aURL, type: aItemType, waiting: true });
    this._manager._ds.addDownload(aName, aURL, aIconURL, aItemType);
    this.id = aID;
  },
  
  removeDownload: function nsItemDownloadTransaction_removeDownload (aURL, aItemType)
  {
    this._manager._ds.removeDownload(aURL, aItemType);
  },
  
  removeAllDownloads: function nsItemDownloadTransaction_removeAllDownloads ()
  {
    for (var i = 0; i < this._downloads.length; ++i)
      this.removeDownload(this._downloads[i].url, this._downloads[i].type);
  },
  
  containsURL: function nsItemDownloadTransaction_containsURL (aURL)
  {
    for (var i = 0; i < this._downloads.length; ++i) {
      if (this._downloads[i].url == aURL)
        return true;
    }
    return false;
  },

  /////////////////////////////////////////////////////////////////////////////  
  // nsIXPIProgressDialog
  onStateChange: function nsItemDownloadTransaction_onStateChange (aIndex, aState, aValue)
  {
    this._manager.onStateChange(this, this._downloads[aIndex].url, aState, aValue);
  },
  
  onProgress: function nsItemDownloadTransaction_onProgress (aIndex, aValue, aMaxValue)
  {
    this._manager.onProgress(this._downloads[aIndex].url, aValue, aMaxValue);
  },
  
  /////////////////////////////////////////////////////////////////////////////
  // nsISupports
  QueryInterface: function nsItemDownloadTransaction_QueryInterface (aIID) 
  {
    if (!aIID.equals(Components.interfaces.nsIXPIProgressDialog) &&
        !aIID.equals(Components.interfaces.nsISupports))
      throw Components.results.NS_ERROR_NO_INTERFACE;
    return this;
  }
};

///////////////////////////////////////////////////////////////////////////////
//
// nsExtensionItemUpdater
//
function nsExtensionItemUpdater(aTargetAppID, aTargetAppVersion, aEM) 
{
  this._appID = aTargetAppID;
  this._appVersion = aTargetAppVersion;
  this._emDS = aEM._ds;
  this._em = aEM;

  getVersionChecker();
}

nsExtensionItemUpdater.prototype = {
  _appID              : "",
  _appVersion         : "",
  _emDS               : null,
  _em                 : null,
  _versionUpdateOnly  : 0,
  _items              : [],
  
  /////////////////////////////////////////////////////////////////////////////
  // nsIExtensionItemUpdater
  //
  // When we check for updates to an item, there are two pieces of information
  // that are returned - 1) info about the newest available version, if any,
  // and 2) info about the currently installed version. The latter is provided
  // primarily to inform the client of changes to the application compatibility 
  // metadata for the current item. Depending on the situation, either 2 or 
  // 1&2 may be what is required.
  //
  // Callers:
  //  1 - nsUpdateService.js, user event
  //      User clicked on the update icon to invoke an update check, 
  //      user clicked on an Extension/Theme and clicked "Update". In this
  //      case we want to update compatibility metadata about the installed
  //      version, and look for newer versions to offer. 
  //  2 - nsUpdateService.js, background event
  //      Timer fired, background update is being performed. In this case
  //      we also want to update compatibility metadata and look for newer
  //      versions.
  //  3 - Mismatch
  //      User upgraded to a newer version of the app, update compatibility
  //      metadata and look for newer versions.
  //  4 - Install Phone Home
  //      User installed an item that was deemed incompatible based only
  //      on the information provided in the item's install.rdf manifest, 
  //      we look ONLY for compatibility updates in this case to determine
  //      whether or not the item can be installed.
  //  
  checkForUpdates: function nsExtensionItemUpdater_checkForUpdates (aItems, aItemCount, 
                                                                    aVersionUpdateOnly) 
  {
    gOS.notifyObservers(null, "Update:Extension:Started", "");
    this._versionUpdateOnly = aVersionUpdateOnly;
    this._items = aItems;
    this._responseCount = aItemCount;
    
    // This is the number of extensions/themes/etc that we found updates for.
    this._updateCount = 0;

    for (var i = 0; i < aItemCount; ++i) {
      var e = this._items[i];
      gOS.notifyObservers(e, "Update:Extension:Item-Started", "");
      (new nsRDFItemUpdater(this)).checkForUpdates(e, aVersionUpdateOnly);
    }
  },
  
  /////////////////////////////////////////////////////////////////////////////
  // nsExtensionItemUpdater
  _applyVersionUpdates: function nsExtensionItemUpdater__applyVersionUpdates (aLocalItem, aRemoteItem)
  {
    var r = this._emDS._getResourceForItem(aLocalItem.id);
    if (!r) return;
    var targetAppInfo = this._emDS.getTargetApplicationInfo(aLocalItem.id, this._emDS, 
                                                            getItemType(r.Value));
    if (gVersionChecker.compare(targetAppInfo.maxVersion, aRemoteItem.maxAppVersion) < 0) {
      // Remotely specified maxVersion is newer than the maxVersion 
      // for the installed Extension. Apply that change to the datasource.
      this._emDS.setTargetApplicationInfo(aLocalItem.id, 
                                          aRemoteItem.minAppVersion, 
                                          aRemoteItem.maxAppVersion,
                                          null, aLocalItem.type);
      
      // If we got here through |checkForMismatches|, this extension has
      // already been disabled, re-enable it.
      if (this._emDS.getItemProperty(aLocalItem.id, "disabled") == "true") 
        this._em.enableExtension(aLocalItem.id);
    }
  },
  
  _isValidUpdate: function nsExtensionItemUpdater__isValidUpdate (aLocalItem, aRemoteItem) 
  {
    var appExtensionsVersion = gPref.getCharPref(PREF_EM_APP_EXTENSIONS_VERSION);

    // Check if the update will only run on a newer version of Firefox. 
    if (aRemoteItem.minAppVersion && 
        gVersionChecker.compare(appExtensionsVersion, aRemoteItem.minAppVersion) < 0) 
      return false;

    // Check if the update will only run on an older version of Firefox. 
    if (aRemoteItem.maxAppVersion && 
        gVersionChecker.compare(appExtensionsVersion, aRemoteItem.maxAppVersion) > 0) 
      return false;
    
    return true;
  },
  
  _checkForDone: function nsExtensionItemUpdater__checkForDone ()
  {
    if (--this._responseCount == 0) {
      if (!this._versionUpdateOnly)
        gPref.setIntPref(PREF_UPDATE_COUNT, this._updateCount); 
          
      gOS.notifyObservers(null, "Update:Extension:Ended", "");
    }
  },
  
  /////////////////////////////////////////////////////////////////////////////
  // nsISupports
  QueryInterface: function nsExtensionItemUpdater_QueryInterface (aIID) 
  {
    if (!aIID.equals(Components.interfaces.nsIExtensionItemUpdater) &&
        !aIID.equals(Components.interfaces.nsISupports))
      throw Components.results.NS_ERROR_NO_INTERFACE;
    return this;
  }  
};

function nsRDFItemUpdater(aUpdater)
{
  this._updater = aUpdater;
}

nsRDFItemUpdater.prototype = {
  _updater            : null,
  _versionUpdateOnly  : 0,
  _item               : null,
  
  checkForUpdates: function (aItem, aVersionUpdateOnly)
  {
    // A preference setting can disable updating for this item
    try {
      if (!gPref.getBoolPref(PREF_EM_ITEM_UPDATE_ENABLED.replace(/%UUID%/, aItem.id))) {
        gOS.notifyObservers(null, "Update:Extension:Item-Ended", "");
        this._updater._checkForDone();
        return;
      }
    }
    catch (e) { }

    this._versionUpdateOnly = aVersionUpdateOnly;
    this._item = aItem;
  
    // Look for a custom update URI: 1) supplied by a pref, 2) supplied by the
    // install manifest, 3) the default configuration
    try {
      var dsURI = gPref.getComplexValue(PREF_EM_ITEM_UPDATE_URL.replace(/%UUID%/, aItem.id),
                                        Components.interfaces.nsIPrefLocalizedString).data;
    }
    catch (e) { }
    if (!dsURI)
      dsURI = aItem.updateRDF;
    if (!dsURI) {
      dsURI = gPref.getComplexValue(PREF_UPDATE_DEFAULT_URL,
                                    Components.interfaces.nsIPrefLocalizedString).data;
    }
    dsURI = dsURI.replace(/%ITEM_ID%/g, aItem.id);
    dsURI = dsURI.replace(/%ITEM_VERSION%/g, aItem.version);
    dsURI = dsURI.replace(/%ITEM_MAXAPPVERSION%/g, aItem.maxAppVersion);
    dsURI = dsURI.replace(/%APP_ID%/g, this._updater._appID);
    dsURI = dsURI.replace(/%APP_VERSION%/g, this._updater._appVersion);
    dsURI = dsURI.replace(/%REQ_VERSION%/g, 1);
    dsURI = dsURI.replace(/%APP_OS%/g, TARGET_OS);
    
    // escape() does not properly encode + symbols in any embedded FVF strings.
    dsURI = dsURI.replace(/\+/g, "%2B");

    var ds = gRDF.GetDataSource(dsURI);
    var rds = ds.QueryInterface(Components.interfaces.nsIRDFRemoteDataSource)
    if (rds.loaded)
      this.onDatasourceLoaded(ds, aItem);
    else {
      var sink = ds.QueryInterface(Components.interfaces.nsIRDFXMLSink);
      sink.addXMLSinkObserver(this);
    }
  },

  onDatasourceLoaded: function nsExtensionItemUpdater_onDatasourceLoaded (aDatasource, aLocalItem)
  {
    ///////////////////////////////////////////////////////////////////////////    
    // The extension update RDF file looks something like this:
    //
    //  <RDF:Description about="urn:mozilla:extension:{GUID}">
    //    <em:updates>
    //      <RDF:Seq>
    //        <RDF:li resource="urn:mozilla:extension:{GUID}:4.9"/>
    //        <RDF:li resource="urn:mozilla:extension:{GUID}:5.0"/>
    //      </RDF:Seq>
    //    </em:updates>
    //    <!-- the version of the extension being offered -->
    //    <em:version>5.0</em:version>
    //    <em:updateLink>http://www.mysite.com/myext-50.xpi</em:updateLink>
    //  </RDF:Description>
    //
    //  <RDF:Description about="urn:mozilla:extension:{GUID}:4.9">
    //    <em:version>4.9</em:version>
    //    <em:targetApplication>
    //      <RDF:Description>
    //        <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
    //        <em:minVersion>0.9</em:minVersion>
    //        <em:maxVersion>1.0</em:maxVersion>
    //        <em:updateLink>http://www.mysite.com/myext-49.xpi</em:updateLink>
    //      </RDF:Description>
    //    </em:targetApplication>
    //  </RDF:Description>  
    //
    // If we get here because the following happened:
    // 1) User was using Firefox 0.9 with ExtensionX 0.5 (minVersion 0.8, 
    //    maxVersion 0.9 for Firefox)
    // 2) User upgraded Firefox to 1.0
    // 3) |checkForMismatches| deems ExtensionX 0.5 incompatible with this
    //    new version of Firefox on the basis of its maxVersion
    // 4) ** We reach this point **
    //
    // If the version of ExtensionX (0.5) matches that provided by the 
    // server, then this is a cue that the author updated the rdf file
    // or central repository to say "0.5 is ALSO compatible with Firefox 1.0,
    // no changes are necessary." In this event, the local metadata for
    // installed ExtensionX (0.5) is freshened with the new maxVersion, 
    // and we advance to the next item WITHOUT any download/install 
    // updates.
  
    // Parse the response RDF
    function UpdateData() {}; 
    UpdateData.prototype = { version: "0.0", updateLink: null, 
                             minVersion: "0.0", maxVersion: "0.0" };
    
    var versionUpdate = new UpdateData();
    var newestUpdate  = new UpdateData();

    var newerItem, sameItem;
    
    // Firefox 1.0PR+ update.rdf format
    if (!this._versionUpdateOnly) {
      // Look for newer versions of this item, we only do this in "normal" 
      // mode... see comment by nsExtensionItemUpdater_checkForUpdates 
      // about how we do this in all cases but Install Phone Home - which 
      // only needs to do a version check.
      this._parseV20UpdateInfo(aDatasource, aLocalItem, newestUpdate, false);
      if (!newestUpdate.updateLink) {
        // Firefox 0.9 update.rdf format - does not contain any metadata
        // that can be used for version updates, so performed in the "all updates"
        // mode only. 
        this._parseV10UpdateInfo(aDatasource, aLocalItem, newestUpdate);
      }

      newerItem = Components.classes["@mozilla.org/updates/item;1"]
                            .createInstance(Components.interfaces.nsIUpdateItem);
      newerItem.init(aLocalItem.id, 
                     newestUpdate.version, 
                     newestUpdate.minVersion, 
                     newestUpdate.maxVersion, 
                     aLocalItem.name, 
                     -1, newestUpdate.updateLink, "", "", 
                     aLocalItem.type);
      if (this._updater._isValidUpdate(aLocalItem, newerItem))
        ++this._updater._updateCount;
      else
        newerItem = null;
    }
    
    // Now look for updated version compatibility metadata for the currently
    // installed version...
    this._parseV20UpdateInfo(aDatasource, aLocalItem, versionUpdate, true);

    var result = gVersionChecker.compare(versionUpdate.version, 
                                          aLocalItem.version);
    if (result == 0) {
      // Local version exactly matches the "Version Update" remote version, 
      // Apply changes into local datasource.
      sameItem = Components.classes["@mozilla.org/updates/item;1"]
                           .createInstance(Components.interfaces.nsIUpdateItem);
      sameItem.init(aLocalItem.id, 
                    versionUpdate.version, 
                    versionUpdate.minVersion, 
                    versionUpdate.maxVersion, 
                    aLocalItem.name, 
                    -1, "", "", "", 
                    aLocalItem.type);
      if (!this._versionUpdateOnly) {
        if (this._updater._isValidUpdate(aLocalItem, sameItem)) {
          // Install-time updates are not written to the DS because there is no
          // entry yet, EM just uses the notifications to ascertain (by hand)
          // whether or not there is a remote maxVersion tweak that makes the 
          // item being installed compatible.
          this._updater._applyVersionUpdates(aLocalItem, sameItem);
        }
        else 
          sameItem = null;
      }
    }
    
    gOS.notifyObservers(!this._versionUpdateOnly ? newerItem : sameItem, 
                        "Update:Extension:Item-Ended", "");
    
    // Only one call of this._updater._checkForDone is needed for RDF 
    // responses, since there is only one response per item.
    this._updater._checkForDone();
  },

  // Parses Firefox 0.9 update.rdf format  
  _parseV10UpdateInfo: function nsExtensionItemUpdater__parseV10UpdateInfo (aDataSource, aLocalItem, aUpdateData)
  {
    var extensionRes  = gRDF.GetResource(getItemPrefix(aLocalItem.type) + aLocalItem.id);
    
    aUpdateData.version     = this._getPropertyFromResource(aDataSource, extensionRes, 
                                                            "version", aLocalItem);
    aUpdateData.updateLink  = this._getPropertyFromResource(aDataSource, extensionRes, 
                                                            "updateLink", aLocalItem);
  },
  
  // Get a compulsory property from a resource. Reports an error if the 
  // property was not present. 
  _getPropertyFromResource: function nsExtensionItemUpdater__getPropertyFromResource (aDataSource,
                                                                                      aSourceResource, 
                                                                                      aProperty, 
                                                                                      aLocalItem)
  {
    var rv;
    try {
      var property = gRDF.GetResource(EM_NS(aProperty));
      rv = stringData(aDataSource.GetTarget(aSourceResource, property, true));
      if (rv == "--")
        throw Components.results.NS_ERROR_FAILURE;
    }
    catch (e) { 
      // XXXben show console message "aProperty" not found on aSourceResource. 
      return null;
    }
    return rv;
  },
  
  // Parses Firefox 1.0RC1+ update.rdf format
  _parseV20UpdateInfo: function nsExtensionItemUpdater__parseV20UpdateInfo (aDataSource, 
                                                                            aLocalItem, 
                                                                            aUpdateData, 
                                                                            aVersionUpdatesOnly)
  {
    var extensionRes  = gRDF.GetResource(getItemPrefix(aLocalItem.type) + aLocalItem.id);

    var updatesArc = gRDF.GetResource(EM_NS("updates"));
    var updates = aDataSource.GetTarget(extensionRes, updatesArc, true);
    
    try {
      updates = updates.QueryInterface(Components.interfaces.nsIRDFResource);
    }
    catch (e) { return; }
    
    var cu = Components.classes["@mozilla.org/rdf/container-utils;1"]
                       .getService(Components.interfaces.nsIRDFContainerUtils);
    if (cu.IsContainer(aDataSource, updates)) {
      var c = Components.classes["@mozilla.org/rdf/container;1"]
                        .getService(Components.interfaces.nsIRDFContainer);
      c.Init(aDataSource, updates);

      // In "all update types" mode, we look for newer versions, starting with the 
      // current installed version.
      if (!aVersionUpdatesOnly) 
        aUpdateData.version = aLocalItem.version;

      var versions = c.GetElements();
      while (versions.hasMoreElements()) {
        // There are two different methodologies for collecting version 
        // information depending on whether or not we've bene invoked in 
        // "version updates only" mode or "version+newest" mode. 
        var version = versions.getNext().QueryInterface(Components.interfaces.nsIRDFResource);
        this._parseV20Update(aDataSource, version, aLocalItem, aUpdateData, aVersionUpdatesOnly);
        if (aVersionUpdatesOnly && aUpdateData.updateLink)
          break;
      }
    }
  },
  
  _parseV20Update: function nsExtensionItemUpdater__parseV20Update (aDataSource,
                                                                    aUpdateResource,
                                                                    aLocalItem,
                                                                    aUpdateData,
                                                                    aVersionUpdatesOnly)
  {
    var version = this._getPropertyFromResource(aDataSource, aUpdateResource, 
                                                "version", aLocalItem);
    var taArc = gRDF.GetResource(EM_NS("targetApplication"));
    var targetApps = aDataSource.GetTargets(aUpdateResource, taArc, true);
    while (targetApps.hasMoreElements()) {
      var targetApp = targetApps.getNext().QueryInterface(Components.interfaces.nsIRDFResource);
      var id = this._getPropertyFromResource(aDataSource, targetApp, "id", aLocalItem);
      if (id != this._updater._appID)
        continue;
      
      var result = gVersionChecker.compare(version, aLocalItem.version);
      if (aVersionUpdatesOnly ? result == 0 : result > 0) {
        aUpdateData.version = version;
        aUpdateData.updateLink = this._getPropertyFromResource(aDataSource, targetApp, "updateLink", aLocalItem);
        aUpdateData.minVersion = this._getPropertyFromResource(aDataSource, targetApp, "minVersion", aLocalItem);
        aUpdateData.maxVersion = this._getPropertyFromResource(aDataSource, targetApp, "maxVersion", aLocalItem);
      }
    }
  },
  
  onDatasourceError: function nsExtensionItemUpdater_onDatasourceError (aItem, aError)
  {
    gOS.notifyObservers(aItem, "Update:Extension:Item-Error", aError);
    this._updater._checkForDone();
  },

  /////////////////////////////////////////////////////////////////////////////
  // nsIRDFXMLSinkObserver
  onBeginLoad: function(aSink)
  {
  },
  onInterrupt: function(aSink)
  {
  },
  onResume: function(aSink)
  {
  },
  
  onEndLoad: function(aSink)
  {
    try {
      aSink.removeXMLSinkObserver(this);
      
      var ds = aSink.QueryInterface(Components.interfaces.nsIRDFDataSource);
      this.onDatasourceLoaded(ds, this._item);
    }
    catch (e) { }
  },
  
  onError: function(aSink, aStatus, aErrorMsg)
  {
    try {
      aSink.removeXMLSinkObserver(this);
      
      this.onDatasourceError(this._item, aStatus.toString());
    }
    catch (e) { }
  }
};

///////////////////////////////////////////////////////////////////////////////
//
// nsExtensionsDataSource
//
function nsExtensionsDataSource()
{
}

nsExtensionsDataSource.prototype = {
  _appExtensions      : null,
  _profileExtensions  : null,  
  _composite          : null,
  safeMode            : false,
  
  _emR: function nsExtensionsDataSource__emR (aProperty) 
  {
    return gRDF.GetResource(EM_NS(aProperty));
  },
  
  _emL: function nsExtensionsDataSource__emL (aLiteral)
  {
    return gRDF.GetLiteral(aLiteral);
  },
  
  isCompatible: function nsExtensionsDataSource__isCompatible (aDS, aSource)
  {
    // XXXben - cheap hack. Our bundled items are always compatible. 
    if (aSource.EqualsNode(gRDF.GetResource(getItemPrefix(nsIUpdateItem.TYPE_THEME) + "{972ce4c6-7e08-4474-a285-3208198ce6fd}")) || 
        aSource.EqualsNode(gRDF.GetResource(getItemPrefix(nsIUpdateItem.TYPE_EXTENSION) + "{641d8d09-7dda-4850-8228-ac0ab65e2ac9}")))
      return true;
      
    var appVersion = gPref.getCharPref(PREF_EM_APP_EXTENSIONS_VERSION);
    var appID = gApp.ID;
    
    var targets = aDS.GetTargets(aSource, this._emR("targetApplication"), true);
    var idRes = this._emR("id");
    var minVersionRes = this._emR("minVersion");
    var maxVersionRes = this._emR("maxVersion");
    while (targets.hasMoreElements()) {
      var targetApp = targets.getNext().QueryInterface(Components.interfaces.nsIRDFResource);
      var id          = stringData(aDS.GetTarget(targetApp, idRes, true));
      var minVersion  = stringData(aDS.GetTarget(targetApp, minVersionRes, true));
      var maxVersion  = stringData(aDS.GetTarget(targetApp, maxVersionRes, true));

      if (id == appID) {
        var versionChecker = getVersionChecker();
        return ((versionChecker.compare(appVersion, minVersion) >= 0) &&
                (versionChecker.compare(appVersion, maxVersion) <= 0));
      }
    }
    return false;
  },
  
  getIncompatibleItemList: function nsExtensionsDataSource_getIncompatibleItemList (aAppID, aAppVersion, aItemType)
  {
    var items = [];
    var roots = getItemRoots(aItemType);
    for (var i = 0; i < roots.length; ++i) {    
      var ctr = Components.classes["@mozilla.org/rdf/container;1"]
                          .createInstance(Components.interfaces.nsIRDFContainer);
      ctr.Init(this._composite, gRDF.GetResource(roots[i]));
      
      var elements = ctr.GetElements();
      while (elements.hasMoreElements()) {
        var e = elements.getNext().QueryInterface(Components.interfaces.nsIRDFResource);
        var itemType = getItemType(e.Value);
        if (itemType != -1 && !this.isCompatible(this, e))
          items.push(this.getItemForID(stripPrefix(e.Value, itemType)));
      }
    }
    return items;
  },
  
  getItemList: function nsExtensionsDataSource_getItemList(aItemID, aType, aCountRef)
  {
    var items = [];
    if (aItemID)
      items.push(this.getItemForID(aItemID));
    else {
      var roots = getItemRoots(aType);
      for (var i = 0; i < roots.length; ++i) {
        var ctr = Components.classes["@mozilla.org/rdf/container;1"]
                            .createInstance(Components.interfaces.nsIRDFContainer);
        ctr.Init(this, gRDF.GetResource(roots[i]));
        
        var elements = ctr.GetElements();
        while (elements.hasMoreElements()) {
          var e = elements.getNext().QueryInterface(Components.interfaces.nsIRDFResource);
          var itemType = getItemType(e.Value);
          if (itemType != -1)
            items.push(this.getItemForID(stripPrefix(e.Value, itemType)));
        }
      }
    }
    aCountRef.value = items.length;
    return items;
  },

  // XXXben this function is a little weird since it returns an array of strings, not
  // an array of nsIUpdateItems...  
  getItemsWithFlagSet: function nsExtensionsDataSource_getItemsWithFlagSet (aFlag)
  {
    var items = [];
    var sources = this.GetSources(this._emR(aFlag), this._emL("true"), true);
    while (sources.hasMoreElements()) {
      var e = sources.getNext().QueryInterface(Components.interfaces.nsIRDFResource);
      
      items.push(stripPrefix(e.Value, getItemType(e.Value)));
    }
    return items;
  },
  
  getItemsWithFlagUnset: function nsExtensionsDataSource_getItemsWithFlagUnset (aFlag, aItemType)
  {
    var items = [];
    
    var roots = getItemRoots(aItemType);
    for (var i = 0; i < roots.length; ++i) {
      var ctr = Components.classes["@mozilla.org/rdf/container;1"]
                          .createInstance(Components.interfaces.nsIRDFContainer);
      ctr.Init(this, gRDF.GetResource(roots[i]));
      
      var elements = ctr.GetElements();
      while (elements.hasMoreElements()) {
        var e = elements.getNext().QueryInterface(Components.interfaces.nsIRDFResource);
        if (getItemType(e.Value) != -1) {
          var value = this.GetTarget(e, this._emR(aFlag), true);
          if (!value)
            items.push(stripPrefix(e.Value, getItemType(e.Value)));
        }
      }
    }
    return items;
  },
  
  getItemForID: function nsExtensionsDataSource_getItemForID (aItemID)
  {
    var item = Components.classes["@mozilla.org/updates/item;1"]
                         .createInstance(Components.interfaces.nsIUpdateItem);
                         
    var r = this._getResourceForItem(aItemID);
    if (!r)
      return null;
    
    var targetAppInfo = this.getTargetApplicationInfo(aItemID, this, getItemType(r.Value));
    item.init(aItemID, 
              this.getItemProperty(aItemID, "version"), 
              targetAppInfo ? targetAppInfo.minVersion : "",
              targetAppInfo ? targetAppInfo.maxVersion : "",
              this.getItemProperty(aItemID, "name"),
              -1, 
              "", /* XPI Update URL */
              this.getItemProperty(aItemID, "iconURL"), 
              this.getItemProperty(aItemID, "updateURL"), 
              getItemType(r.Value));
    return item;
  },
  
  isProfileItem: function nsExtensionsDataSource_isProfileItem (aItemID)
  {
    return this.getItemProperty(aItemID, "installLocation") != "global";
  },
  
  _setProperty: function nsExtensionsDataSource__setProperty (aDS, aSource, aProperty, aNewValue)
  {
    var oldValue = aDS.GetTarget(aSource, aProperty, true);
    if (oldValue) {
      if (aNewValue)
        aDS.Change(aSource, aProperty, oldValue, aNewValue);
      else
        aDS.Unassert(aSource, aProperty, oldValue);
    }
    else if (aNewValue)
      aDS.Assert(aSource, aProperty, aNewValue, true);
  },
  
  // Given a GUID, get the RDF resource representing the item. This
  // will be of the form urn:mozilla:extension:{GUID} or 
  // urn:mozilla:theme:{GUID} depending on the item type. 
  _getResourceForItem: function nsExtensionsDataSource__getResourceForItem(aItemID)
  {
    var res = null;
    
    // We can try and infer the resource URI from presence in one of the 
    // item lists.
    var rdfc = Components.classes["@mozilla.org/rdf/container;1"]
                         .createInstance(Components.interfaces.nsIRDFContainer);
    rdfc.Init(this, gRDF.GetResource(ROOT_EXTENSION));
    res = gRDF.GetResource(PREFIX_EXTENSION + aItemID);
    if (rdfc.IndexOf(res) != -1)
      return res;
    
    rdfc.Init(this, gRDF.GetResource(ROOT_THEME));
    res = gRDF.GetResource(PREFIX_THEME + aItemID);
    if (rdfc.IndexOf(res) != -1)
      return res;

    return null;
  },

  getTargetApplicationInfo: function nsExtensionsDataSource_getTargetApplicationInfo (aExtensionID, aDataSource, aType)
  {
    var internalName = this.getItemProperty(aExtensionID, "internalName");
    // The default theme is always compatible. 
    if (internalName == KEY_DEFAULT_THEME) {
      var ver = gPref.getCharPref(PREF_EM_APP_EXTENSIONS_VERSION);
      return { minVersion: ver, maxVersion: ver };
    }

    var appID = gApp.ID;
    var r = gRDF.GetResource(getItemPrefix(aType) + aExtensionID);
    var targetApps = aDataSource.GetTargets(r, this._emR("targetApplication"), true);
    if (!targetApps.hasMoreElements()) {
      r = gRDF.GetResource("urn:mozilla:install-manifest"); 
      targetApps = aDataSource.GetTargets(r, this._emR("targetApplication"), true); 
    }
    while (targetApps.hasMoreElements()) {
      var targetApp = targetApps.getNext().QueryInterface(Components.interfaces.nsIRDFResource);
      if (targetApp) {
        try {
          var id = stringData(aDataSource.GetTarget(targetApp, this._emR("id"), true));
          if (id != appID) // Different target application
            continue;
          
          return { minVersion: stringData(aDataSource.GetTarget(targetApp, this._emR("minVersion"), true)),
                   maxVersion: stringData(aDataSource.GetTarget(targetApp, this._emR("maxVersion"), true)) };
        }
        catch (e) { 
          continue;
        }
      }
    }
    return null;
  },
  
  setTargetApplicationInfo: function nsExtensionsDataSource_setTargetApplicationInfo(aExtensionID, aMinVersion, 
                                                                                     aMaxVersion, aDataSource, aType)
  {
    var targetDataSource = aDataSource;
    if (!targetDataSource)
      targetDataSource = this;
      
    var appID = gApp.ID;
    var r = gRDF.GetResource(getItemPrefix(aType) + aExtensionID);
    var targetApps = targetDataSource.GetTargets(r, this._emR("targetApplication"), true);
    if (!targetApps.hasMoreElements()) {
      r = gRDF.GetResource("urn:mozilla:install-manifest"); 
      targetApps = aDataSource.GetTargets(r, this._emR("targetApplication"), true); 
    }
    while (targetApps.hasMoreElements()) {
      var targetApp = targetApps.getNext().QueryInterface(Components.interfaces.nsIRDFResource);
      if (targetApp) {
        var id = stringData(targetDataSource.GetTarget(targetApp, this._emR("id"), true));
        if (id != appID) // Different target application
          continue;
        
        if (!aDataSource) {
          var isProfile = this.isProfileItem(aExtensionID);
          targetDataSource = isProfile ? this._profileExtensions : this._appExtensions;
        }
        this._setProperty(targetDataSource, targetApp, this._emR("minVersion"), this._emL(aMinVersion));
        this._setProperty(targetDataSource, targetApp, this._emR("maxVersion"), this._emL(aMaxVersion));
        
        if (!aDataSource)
          this._flush(isProfile);
      }
    }
  },
  
  getItemProperty: function nsExtensionsDataSource_getItemProperty (aItemID, aProperty)
  { 
    var item = this._getResourceForItem(aItemID);
    if (!item) {
      dump("*** getItemProperty failing for lack of an item. This means getResourceForItem \
                failed to locate a resource for aItemID (item ID = " + aItemID + ", property = " + aProperty + ")\n");
    }
    else 
      return this._getItemProperty(item, aProperty);
    return undefined;
  },
  
  _getItemProperty: function nsExtensionsDataSource__getItemProperty (aItemResource, aProperty)
  {
    try {
      return this.GetTarget(aItemResource, this._emR(aProperty), true).QueryInterface(Components.interfaces.nsIRDFLiteral).Value;
    }
    catch (e) {}
    return "";
  },
  
  setItemProperty: function nsExtensionsDataSource_setItemProperty(
    aItemID, aPropertyArc, aPropertyValue, aIsProfile, aItemType)
  {
    var item = gRDF.GetResource(getItemPrefix(aItemType) + aItemID);
    var ds = aIsProfile ? this._profileExtensions : this._appExtensions;
    this._setProperty(ds, item, aPropertyArc, aPropertyValue);

    this._flush(aIsProfile);  
  },

  insertForthcomingItem: function nsExtensionsDataSource_insertForthcomingItem (aItemID, aItemType, aIsProfile)
  {
    // Get the target container and resource
    var targetDS = aIsProfile ? this._profileExtensions : this._appExtensions;
    var ctr = Components.classes["@mozilla.org/rdf/container;1"]
                        .createInstance(Components.interfaces.nsIRDFContainer);
    ctr.Init(targetDS, gRDF.GetResource(getItemRoot(aItemType)));

    var targetRes = gRDF.GetResource(getItemPrefix(aItemType) + aItemID);
    // Don't bother adding the extension to the list if it's already there. 
    // (i.e. we're upgrading)
    var oldIndex = ctr.IndexOf(targetRes);
    if (oldIndex == -1)
      ctr.AppendElement(targetRes);

    this._flush(aIsProfile);
  }, 

  removeItemFromContainer: function nsExtensionsDataSource_removeItemFromContainer(aItemID, aItemType, aIsProfile)
  {
    var targetDS = aIsProfile ? this._profileExtensions : this._appExtensions;
    var ctr = Components.classes["@mozilla.org/rdf/container;1"]
                        .createInstance(Components.interfaces.nsIRDFContainer);
    ctr.Init(targetDS, gRDF.GetResource(getItemRoot(aItemType)));
    
    var item = gRDF.GetResource(getItemPrefix(aItemType) + aItemID);
    ctr.RemoveElement(item, true);
    
    this._flush(aIsProfile);
  },

  // Removes a corrupt item entry from the extension list added due to 
  // buggy code in previous EM versions!  
  removeCorruptItem: function nsExtensionsDataSource_removeCorruptItem (aItemID, aItemType, aIsProfile)
  {
    this.removeItemMetadata(aItemID, aItemType);
    this.removeItemFromContainer(aItemID, aItemType, aIsProfile);
  },

  // Removes a corrupt download entry from the list.   
  removeCorruptDLItem: function nsExtensionsDataSource_removeCorruptDLItem (aItemURI, aItemType)
  {
    var itemResource = gRDF.GetResource(aItemURI);
    var itemRoot = gRDF.GetResource(getItemRoot(aItemType));
    var dses = [this._profileExtensions, this._appExtensions];
    var isProfile = [true, false];
    for (var i = 0; i < dses.length; ++i) {
      var ctr = Components.classes["@mozilla.org/rdf/container;1"]
                          .createInstance(Components.interfaces.nsIRDFContainer);
      ctr.Init(dses[i], itemRoot);
      if (ctr.IndexOf(itemResource) != -1) {
        ctr.RemoveElement(itemResource, true);
        this._cleanResource(itemResource, dses[i]);
        this._flush(isProfile[i]);
        break;
      }
    }
  },
  
  addItemMetadata: function nsExtensionsDataSource_addItemMetadata (aItemID, aItemType, aSourceDS, aIsProfile)
  {
    var targetDS = aIsProfile ? this._profileExtensions : this._appExtensions;
    var targetRes = gRDF.GetResource(getItemPrefix(aItemType) + aItemID);

    // Copy the assertions over from the source datasource. 
    
    // Assert properties with single values
    var singleProps = ["version", "name", "description", "creator", "homepageURL", 
                       "updateURL", "updateService", "optionsURL", "aboutURL", 
                       "iconURL", "internalName"];

    // Global extensions and themes can also be locked (can't be removed or disabled).
    if (!aIsProfile)
      singleProps = singleProps.concat(["locked"]);
    var sourceRes = gRDF.GetResource("urn:mozilla:install-manifest");
    for (var i = 0; i < singleProps.length; ++i) {
      var property = this._emR(singleProps[i]);
      var literal = aSourceDS.GetTarget(sourceRes, property, true);
      if (!literal)
        continue; // extension didn't specify this property, no big deal, continue.
        
      var val = literal.QueryInterface(Components.interfaces.nsIRDFLiteral).Value;
      
      var oldValue = targetDS.GetTarget(targetRes, property, true);
      if (!oldValue)
        targetDS.Assert(targetRes, property, literal, true);
      else
        targetDS.Change(targetRes, property, oldValue, literal);
    }    

    // Assert properties with multiple values    
    var manyProps = ["contributor"];
    for (var i = 0; i < singleProps.length; ++i) {
      var property = this._emR(manyProps[i]);
      var literals = aSourceDS.GetTargets(sourceRes, property, true);
      
      var oldValues = targetDS.GetTargets(targetRes, property, true);
      while (oldValues.hasMoreElements()) {
        var oldValue = oldValues.getNext().QueryInterface(Components.interfaces.nsIRDFNode);
        targetDS.Unassert(targetRes, property, oldValue);
      }
      while (literals.hasMoreElements()) {
        var literal = literals.getNext().QueryInterface(Components.interfaces.nsIRDFNode);
        targetDS.Assert(targetRes, property, literal, true);
      }
    }
    
    // Version/Dependency Info
    var versionProps = ["targetApplication", "requires"];
    var idRes = this._emR("id");
    var minVersionRes = this._emR("minVersion");
    var maxVersionRes = this._emR("maxVersion");
    for (var i = 0; i < versionProps.length; ++i) {
      var property = this._emR(versionProps[i]);
      var newVersionInfos = aSourceDS.GetTargets(sourceRes, property, true);
      
      var oldVersionInfos = targetDS.GetTargets(targetRes, property, true);
      while (oldVersionInfos.hasMoreElements()) {
        var oldVersionInfo = oldVersionInfos.getNext().QueryInterface(Components.interfaces.nsIRDFResource);
        this._cleanResource(oldVersionInfo, targetDS);
        targetDS.Unassert(targetRes, property, oldVersionInfo);
      }
      while (newVersionInfos.hasMoreElements()) {
        var newVersionInfo = newVersionInfos.getNext().QueryInterface(Components.interfaces.nsIRDFResource);
        var anon = gRDF.GetAnonymousResource();
        targetDS.Assert(anon, idRes, aSourceDS.GetTarget(newVersionInfo, idRes, true), true);
        targetDS.Assert(anon, minVersionRes, aSourceDS.GetTarget(newVersionInfo, minVersionRes, true), true);
        targetDS.Assert(anon, maxVersionRes, aSourceDS.GetTarget(newVersionInfo, maxVersionRes, true), true);
        targetDS.Assert(targetRes, property, anon, true);
      }
    }
    
    this._flush(aIsProfile);
  },
  
  lockUnlockItem: function nsExtensionsDataSource_lockUnlockItem (aItemID, aLocked)
  {
    var item = this._getResourceForItem(aItemID);
    if (item) {
      var val = aLocked ? this._emL("true") : this._emL("false");
      this.setItemProperty(aItemID, this._emR("locked"), val, false, getItemType(item.Value));
      this._flush(false);
    }
  },  
  
  enableExtension: function nsExtensionsDataSource_enableExtension (aExtensionID)
  {
    this.setItemProperty(aExtensionID, this._emR("toBeEnabled"), 
                         this._emL("true"), this.isProfileItem(aExtensionID), 
                         nsIUpdateItem.TYPE_EXTENSION);
    this.setItemProperty(aExtensionID, this._emR("toBeDisabled"), 
                         null, this.isProfileItem(aExtensionID), 
                         nsIUpdateItem.TYPE_EXTENSION);
    this.setItemProperty(aExtensionID, this._emR("disabled"), 
                         null, this.isProfileItem(aExtensionID), 
                         nsIUpdateItem.TYPE_EXTENSION);
  },
  
  disableExtension: function nsExtensionsDataSource_disableExtension (aExtensionID)
  {
    this.setItemProperty(aExtensionID, this._emR("toBeDisabled"), 
                         this._emL("true"), this.isProfileItem(aExtensionID), 
                         nsIUpdateItem.TYPE_EXTENSION);
    this.setItemProperty(aExtensionID, this._emR("toBeEnabled"), 
                         null, this.isProfileItem(aExtensionID), 
                         nsIUpdateItem.TYPE_EXTENSION);
    this.setItemProperty(aExtensionID, this._emR("disabled"), 
                         this._emL("true"), this.isProfileItem(aExtensionID), 
                         nsIUpdateItem.TYPE_EXTENSION);
  },
  
  uninstallExtension: function nsExtensionsDataSource_uninstallExtension (aExtensionID)
  {
    // We have to do this check BEFORE we unhook all the metadata from this 
    // extension's resource, otherwise we'll think it's a global extension.
    var isProfile = this.isProfileItem(aExtensionID);
    
    this.setItemProperty(aExtensionID, this._emR("toBeInstalled"), 
                         null, isProfile, 
                         nsIUpdateItem.TYPE_EXTENSION);
    this.setItemProperty(aExtensionID, this._emR("toBeUninstalled"), 
                         this._emL("true"), isProfile, 
                         nsIUpdateItem.TYPE_EXTENSION);
    this._flush(isProfile);
  },
  
  doneInstallingTheme: function nsExtensionsDataSource_doneInstallingTheme (aThemeID)
  {
    // Notify observers of a change in the iconURL property to cause the UI to
    // refresh.
    var theme = this._getResourceForItem(aThemeID);
    var iconURLArc = this._emR("iconURL");
    var iconURL = this.GetTarget(theme, iconURLArc, true);
    if (theme, iconURLArc, iconURL) {
      for (var i = 0; i < this._observers.length; ++i)
        this._observers[i].onAssert(this, theme, iconURLArc, iconURL);
    }
  },
  
  enableTheme: function nsExtensionsDataSource_enableTheme (aThemeID)
  {
    this.setItemProperty(aThemeID, this._emR("disabled"), 
                         null, this.isProfileItem(aThemeID), 
                         nsIUpdateItem.TYPE_THEME);
  },
    
  disableTheme: function nsExtensionsDataSource_disableTheme (aThemeID)
  {
    this.setItemProperty(aThemeID, this._emR("disabled"), 
                         this._emL("true"), this.isProfileItem(aThemeID), 
                         nsIUpdateItem.TYPE_THEME);
  },
  
  uninstallTheme: function nsExtensionsDataSource_uninstallTheme(aThemeID)
  {
    // We have to do this check BEFORE we unhook all the metadata from this 
    // extension's resource, otherwise we'll think it's a global extension.
    var isProfile = this.isProfileItem(aThemeID);
        
    // Clean the extension resource
    this.removeItemMetadata(aThemeID, nsIUpdateItem.TYPE_THEME);
    
    var uninstaller = new nsThemeUninstaller(this);
    uninstaller.uninstall(aThemeID, isProfile);  
    
    // Do this LAST since inferences are made about an item based on
    // what container it's in.
    this.removeItemFromContainer(aThemeID, nsIUpdateItem.TYPE_THEME, isProfile);
  },
  
  // Cleans the resource of all its assertionss
  removeItemMetadata: function nsExtensionsDataSource_removeItemMetadata (aItemID, aItemType)
  {
    var item = gRDF.GetResource(getItemPrefix(aItemType) + aItemID);
    var isProfile = this.isProfileItem(aItemID);
    var ds = isProfile ? this._profileExtensions : this._appExtensions;
    
    var resources = ["targetApplication", "requires"];
    for (var i = 0; i < resources.length; ++i) {
      var targetApps = ds.GetTargets(item, this._emR(resources[i]), true);
      while (targetApps.hasMoreElements()) {
        var targetApp = targetApps.getNext().QueryInterface(Components.interfaces.nsIRDFResource);
        this._cleanResource(targetApp, ds);
      }
    }

    this._cleanResource(item, ds);
  },
  
  _cleanResource: function nsExtensionsDataSource__cleanResource (aResource, aDS)
  {
    // Remove outward arcs
    var arcs = aDS.ArcLabelsOut(aResource);
    while (arcs.hasMoreElements()) {
      var arc = arcs.getNext().QueryInterface(Components.interfaces.nsIRDFResource);
      var value = aDS.GetTarget(aResource, arc, true);
      if (value)
        aDS.Unassert(aResource, arc, value);
    }
  },
  
  moveTop: function nsExtensionsDataSource_moveTop (aItemID)
  {
    var extensions = gRDF.GetResource("urn:mozilla:extension:root");
    var item = this._getResourceForItem(aItemID);
    var ds = this._getTargetDSFromSource(item);
    var container = Components.classes["@mozilla.org/rdf/container;1"]
                              .createInstance(Components.interfaces.nsIRDFContainer);
    container.Init(ds, extensions);
    
    var index = container.IndexOf(item);
    if (index > 1) {
      container.RemoveElement(item, false);
      container.InsertElementAt(item, 1, true);
    }
    this._flush(this.isProfileItem(aItemID));
  },
  
  moveUp: function nsExtensionsDataSource_moveUp (aItemID)
  {
    var extensions = gRDF.GetResource("urn:mozilla:extension:root");
    var item = this._getResourceForItem(aItemID);
    var ds = this._getTargetDSFromSource(item);
    var container = Components.classes["@mozilla.org/rdf/container;1"]
                              .createInstance(Components.interfaces.nsIRDFContainer);
    container.Init(ds, extensions);
    
    var item = this._getResourceForItem(aItemID);
    var index = container.IndexOf(item);
    if (index > 1) {
      container.RemoveElement(item, false);
      container.InsertElementAt(item, index - 1, true);
    }
    this._flush(this.isProfileItem(aItemID));
  },
  
  moveDown: function nsExtensionsDataSource_moveDown (aItemID)
  {
    var extensions = gRDF.GetResource("urn:mozilla:extension:root");
    var item = this._getResourceForItem(aItemID);
    var ds = this._getTargetDSFromSource(item);
    var container = Components.classes["@mozilla.org/rdf/container;1"]
                              .createInstance(Components.interfaces.nsIRDFContainer);
    container.Init(ds, extensions);
    
    var item = this._getResourceForItem(aItemID);
    var index = container.IndexOf(item);
    var count = container.GetCount();
    if (index < count) {
      container.RemoveElement(item, true);
      container.InsertElementAt(item, index + 1, true);
    }
    this._flush(this.isProfileItem(aItemID));
  },
  
  isDownloadItem: function nsExtensionsDataSource_isDownloadItem (aItemID)
  {
    return this.getItemProperty(aItemID, "downloadURL") != "";
  },

  addDownload: function nsExtensionsDataSource_addDownload (aName, aURL, aIconURL, aItemType)
  {
    var root = gRDF.GetResource(getItemRoot(aItemType));
    
    var res = gRDF.GetResource(aURL);
    this._setProperty(this._profileExtensions, res, 
                      this._emR("name"),
                      gRDF.GetLiteral(aName))
    this._setProperty(this._profileExtensions, res, 
                      this._emR("version"),
                      gRDF.GetLiteral(" "));
    this._setProperty(this._profileExtensions, res, 
                      this._emR("iconURL"),
                      gRDF.GetLiteral(aIconURL));
    this._setProperty(this._profileExtensions, res, 
                      this._emR("downloadURL"),
                      gRDF.GetLiteral(aURL));

    var ctr = Components.classes["@mozilla.org/rdf/container;1"]
                        .createInstance(Components.interfaces.nsIRDFContainer);
    ctr.Init(this._profileExtensions, root);
    if (ctr.IndexOf(res) == -1)
      ctr.InsertElementAt(res, 1, true);
    
    this._flush(true);
  },
  
  removeDownload: function nsExtensionsDataSource_removeDownload (aURL, aItemType)
  {
    var root = gRDF.GetResource(getItemRoot(aItemType));
    var res = gRDF.GetResource(aURL);
    var ctr = Components.classes["@mozilla.org/rdf/container;1"]
                        .createInstance(Components.interfaces.nsIRDFContainer);
    ctr.Init(this._profileExtensions, root);
    ctr.RemoveElement(res, true);
    this._cleanResource(res, this._profileExtensions);
    
    this._flush(true);
  },
    
  flushProgressInfo: function nsExtensionsDataSource_flushProgressInfo (aData)
  {
    for (var url in aData) {
      var res = gRDF.GetResource(url);
      this._setProperty(this._profileExtensions, res, 
                        this._emR("state"),
                        gRDF.GetIntLiteral(aData[url].state));
      this._setProperty(this._profileExtensions, res, 
                        this._emR("progress"),
                        gRDF.GetIntLiteral(aData[url].progress));
    }
    this._flush(true);
  },   
   
  loadExtensions: function nsExtensionsDataSource_loadExtensions (aProfile)
  {
    var extensionsFile  = getFile(getDirKey(aProfile), 
                                  [DIR_EXTENSIONS, FILE_EXTENSIONS]);
    ensureExtensionsFiles(aProfile);

    var ds = gRDF.GetDataSourceBlocking(getURLSpecFromFile(extensionsFile));
    if (aProfile) {
      this._profileExtensions = ds;
      if (!this._composite) 
        this._composite = Components.classes["@mozilla.org/rdf/datasource;1?name=composite-datasource"]
                                    .createInstance(Components.interfaces.nsIRDFDataSource);
      if (this._appExtensions)
        this._composite.RemoveDataSource(this._appExtensions);
      this._composite.AddDataSource(this._profileExtensions);
      if (this._appExtensions)
        this._composite.AddDataSource(this._appExtensions);  
    }
    else {
      this._appExtensions = ds;
      
      if (!this._composite)
        this._composite = Components.classes["@mozilla.org/rdf/datasource;1?name=composite-datasource"]
                                    .createInstance(Components.interfaces.nsIRDFCompositeDataSource);
      this._composite.AddDataSource(this._appExtensions);
    }
  },
  
  /////////////////////////////////////////////////////////////////////////////
  // nsIRDFDataSource
  get URI()
  {
    return "rdf:extensions";
  },
  
  GetSource: function nsExtensionsDataSource_GetSource (aProperty, aTarget, aTruthValue)
  {
    return this._composite.GetSource(aProperty, aTarget, aTruthValue);
  },
  
  GetSources: function nsExtensionsDataSource_GetSources (aProperty, aTarget, aTruthValue)
  {
    return this._composite.GetSources(aProperty, aTarget, aTruthValue);
  },
  
  _getThemeJARURL: function nsExtensionsDataSource__getThemeJARURL (aSource, aFileName, aFallbackURL)
  {
    var id = stripPrefix(aSource.Value, nsIUpdateItem.TYPE_THEME);
    var chromeDir = getDir(this.isProfileItem(id) ? KEY_PROFILEDIR : KEY_APPDIR, 
                            [DIR_EXTENSIONS, id, DIR_CHROME]);

    var jarFile = null;
    // XXXben hack for pre-configured classic.jar
    if ((!chromeDir.exists() || !chromeDir.directoryEntries.hasMoreElements()) &&
        aSource.EqualsNode(gRDF.GetResource("urn:mozilla:theme:{972ce4c6-7e08-4474-a285-3208198ce6fd}")))
      jarFile = getFile(KEY_APPDIR, ["chrome", "classic.jar"]);
    if (chromeDir.directoryEntries.hasMoreElements() || jarFile) {
      if (!jarFile)
        jarFile = chromeDir.directoryEntries.getNext().QueryInterface(Components.interfaces.nsIFile);

      if (jarFile.exists()) {
        var zipReader = Components.classes["@mozilla.org/libjar/zip-reader;1"]
                                  .createInstance(Components.interfaces.nsIZipReader);
        zipReader.init(jarFile);
        zipReader.open();
        var url = aFallbackURL;
        try {
          zipReader.getEntry(aFileName);
          url = "jar:" + getURLSpecFromFile(jarFile) + "!/" + aFileName; 
        }
        catch (e) { }
        zipReader.close();
        
        if (url)
          return gRDF.GetResource(url);
      }
    }
    return null;
  },
  
  GetTarget: function nsExtensionsDataSource_GetTarget(aSource, aProperty, aTruthValue)
  {
    if (!aSource)
      return null;
      
    if (aProperty.EqualsNode(this._emR("iconURL"))) {
      var itemType = getItemType(aSource.Value);
      if (itemType != -1 && itemType & nsIUpdateItem.TYPE_EXTENSION) {
        var hasIconURL = this._composite.hasArcOut(aSource, aProperty);
        // If the download entry doesn't have a IconURL property, use a
        // generic icon URL instead.
        if (!hasIconURL)
          return gRDF.GetResource("chrome://mozapps/skin/xpinstall/xpinstallItemGeneric.png");
        else {
          var iconURL = this._composite.GetTarget(aSource, aProperty, true);
          iconURL = iconURL.QueryInterface(Components.interfaces.nsIRDFLiteral).Value;
          var cr = Components.classes["@mozilla.org/chrome/chrome-registry;1"]
                             .getService(Components.interfaces.nsIChromeRegistry);
          var ioServ = Components.classes["@mozilla.org/network/io-service;1"]
                                 .getService(Components.interfaces.nsIIOService);
          var uri = ioServ.newURI(iconURL, null, null);
          try {
            cr.convertChromeURL(uri);
          }
          catch(e) {
            // bogus URI, supply a generic icon. 
            return gRDF.GetResource("chrome://mozapps/skin/xpinstall/xpinstallItemGeneric.png");
          }
        }
      }
      else if (itemType != -1 && itemType & nsIUpdateItem.TYPE_THEME) {
        var res = this._getThemeJARURL(aSource, "icon.png", "chrome://mozapps/skin/extensions/themeGeneric.png");
        if (res)
          return res;
      }
    }
    else if (aProperty.EqualsNode(this._emR("previewImage"))) {
      var itemType = getItemType(aSource.Value);
      if (itemType != -1 && itemType & nsIUpdateItem.TYPE_THEME) {
        var res = this._getThemeJARURL(aSource, "preview.png", null);
        if (res)
          return res;
      }
    }
    else if (aProperty.EqualsNode(this._emR("installLocation"))) {
      var arcs = this._profileExtensions.ArcLabelsOut(aSource);
      return arcs.hasMoreElements() ? this._emL("profile") : this._emL("global");
    }
    else if (aProperty.EqualsNode(this._emR("disabled"))) {
      if (this.safeMode) 
        return this._emL("true");
      // fall through to default.
    }
    else if (aProperty.EqualsNode(this._emR("itemType"))) {
      // We can try and infer the type from presence in one of the 
      // item lists.
      var rdfc = Components.classes["@mozilla.org/rdf/container;1"]
                          .createInstance(Components.interfaces.nsIRDFContainer);
      rdfc.Init(this, gRDF.GetResource(ROOT_EXTENSION));
      if (rdfc.IndexOf(aSource) != -1) 
        return this._emL("extension");
    
      rdfc.Init(this, gRDF.GetResource(ROOT_THEME));
      if (rdfc.IndexOf(aSource) != -1) 
        return this._emL("theme");
    }
    else if (aProperty.EqualsNode(this._emR("compatible"))) {
      var type = getItemType(aSource.Value);
      var id = stripPrefix(aSource.Value, type);
      var targetAppInfo = this.getTargetApplicationInfo(id, this, type);
      if (!targetAppInfo)
        return this._emL("false");
      getVersionChecker();
      
      var appVersion = gPref.getCharPref(PREF_EM_APP_EXTENSIONS_VERSION);
      if (gVersionChecker.compare(targetAppInfo.maxVersion, appVersion) < 0 || 
          gVersionChecker.compare(appVersion, targetAppInfo.minVersion) < 0) {
        // OK, this item is incompatible. 
        return this._emL("false");
      }
      return this._emL("true");
    }
    else if (aProperty.EqualsNode(this._emR("displayDescription"))) {
      // We have a separate property for the description of the extension items
      // which is displayed in the EM list - because we overload this value with
      // alternative messages when the extension is disabled because of 
      // incompatibility.
      var disabled = this.getItemProperty(stripPrefix(aSource.Value, getItemType(aSource.Value)), 
                                          "disabled");
      if (disabled == "true") {
        // See if this item was disabled because it was incompatible. 
        // XXXben potential visual-glitch bug here with extensions whose install.rdf 
        //        manifests state that they are incompatible but when phone home checking
        //        reveals that they are compatible and they are installed the 
        //        incompatible metadata is written anyway and will remain in the ds
        //        until the next background update check corrects it - this means that
        //        when a compatible extension is installed in this manner it is 
        //        likely that when it is disabled it will show this special-case
        //        error message.
        var compatible = this.getItemProperty(stripPrefix(aSource.Value, getItemType(aSource.Value)), 
                                              "compatible");
        if (compatible != "true") {
          var sbs = Components.classes["@mozilla.org/intl/stringbundle;1"]
                              .getService(Components.interfaces.nsIStringBundleService);
          var extensionStrings = sbs.createBundle("chrome://mozapps/locale/extensions/extensions.properties");
          var brandStrings = sbs.createBundle("chrome://global/locale/brand.properties");
          var brandShortName = brandStrings.GetStringFromName("brandShortName");
          var appVersion = gApp.version;
          var incompatibleMessage = extensionStrings.formatStringFromName("incompatibleExtension", 
                                                                          [brandShortName, appVersion], 2);
          return this._emL(incompatibleMessage);
        }
      }
      // Use the "description" property. 
      return this.GetTarget(aSource, this._emR("description"), aTruthValue);
    }
    else if (aProperty.EqualsNode(this._emR("name")) || 
             aProperty.EqualsNode(this._emR("description")) || 
             aProperty.EqualsNode(this._emR("creator")) || 
             aProperty.EqualsNode(this._emR("homepageURL"))) {
      // These are localizable properties that a language pack supplied by the 
      // Extension may override.          
      var prefName = PREF_EM_EXTENSION_FORMAT.replace(/%UUID%/, 
                                                      stripPrefix(aSource.Value, 
                                                                  nsIUpdateItem.TYPE_EXTENSION)) + 
                      stripPropertyPrefix(aProperty.Value, EM_NS_PREFIX);
      try {
        var value = gPref.getComplexValue(prefName, 
                                          Components.interfaces.nsIPrefLocalizedString);
        if (value.data) 
          return this._emL(value.data);
      }
      catch (e) {
      }
    }
    
    return this._composite.GetTarget(aSource, aProperty, aTruthValue);
  },
  
  GetTargets: function nsExtensionsDataSource_GetTargets (aSource, aProperty, aTruthValue)
  {
    if (aProperty.EqualsNode(this._emR("name")) ||
        aProperty.EqualsNode(this._emR("contributor"))) {
      // These are localizable properties that a language pack supplied by the 
      // Extension may override.          
      var contributors = [];
      var prefName = PREF_EM_EXTENSION_FORMAT.replace(/%UUID%/, 
                                                      stripPrefix(aSource.Value, 
                                                                  nsIUpdateItem.TYPE_EXTENSION)) + 
                      stripPropertyPrefix(aProperty.Value, EM_NS_PREFIX);
      var i = 0;
      do {
        try {
          var value = gPref.getComplexValue(prefName + "." + ++i, 
                                            Components.interfaces.nsIPrefLocalizedString);
          if (value.data) 
            contributors.push(this._emL(value.data));
        }
        catch (e) {
          try {
            var value = gPref.getComplexValue(prefName, 
                                              Components.interfaces.nsIPrefLocalizedString);
            if (value.data) 
              contributors.push(this._emL(value.data));
          }
          catch (e) {
          }
          break;
        }
      }
      while (1);
      if (contributors.length > 0)
        return new ArrayEnumerator(contributors);
    }
    return this._composite.GetTargets(aSource, aProperty, aTruthValue);
  },
  
  _getTargetDSFromSource: function nsExtensionsDataSource__getTargetDSFromSource (aSource)
  {
    var itemID = stripPrefix(aSource.Value, nsIUpdateItem.TYPE_ADDON);
    return this.isProfileItem(itemID) ? this._profileExtensions : this._appExtensions;
  },
  
  Assert: function nsExtensionsDataSource_Assert (aSource, aProperty, aTarget, aTruthValue)
  {
    var targetDS = this._getTargetDSFromSource(aSource);
    targetDS.Assert(aSource, aProperty, aTarget, aTruthValue);
  },
  
  Unassert: function nsExtensionsDataSource_Unassert (aSource, aProperty, aTarget)
  {
    var targetDS = this._getTargetDSFromSource(aSource);
    targetDS.Unassert(aSource, aProperty, aTarget);
  },
  
  Change: function nsExtensionsDataSource_Change (aSource, aProperty, aOldTarget, aNewTarget)
  {
    var targetDS = this._getTargetDSFromSource(aSource);
    targetDS.Change(aSource, aProperty, aOldTarget, aNewTarget);
  },

  Move: function nsExtensionsDataSource_Move (aSource, aNewSource, aProperty, aTarget)
  {
    var targetDS = this._getTargetDSFromSource(aSource);
    targetDS.Move(aSource, aNewSource, aProperty, aTarget);
  },
  
  HasAssertion: function nsExtensionsDataSource_HasAssertion (aSource, aProperty, aTarget, aTruthValue)
  {
    if (!aSource || !aProperty || !aTarget)
      return false;
    return this._composite.HasAssertion(aSource, aProperty, aTarget, aTruthValue);
  },
  
  _observers: [],
  AddObserver: function nsExtensionsDataSource_AddObserver (aObserver)
  {
    for (var i = 0; i < this._observers.length; ++i) {
      if (this._observers[i] == aObserver) 
        return;
    }
    this._observers.push(aObserver);
    this._composite.AddObserver(aObserver);
  },
  
  RemoveObserver: function nsExtensionsDataSource_RemoveObserver (aObserver)
  {
    for (var i = 0; i < this._observers.length; ++i) {
      if (this._observers[i] == aObserver) 
        this._observers.splice(i, 1);
    }
    this._composite.RemoveObserver(aObserver);
  },
  
  ArcLabelsIn: function nsExtensionsDataSource_ArcLabelsIn (aNode)
  {
    return this._composite.ArcLabelsIn(aNode);
  },
  
  ArcLabelsOut: function nsExtensionsDataSource_ArcLabelsOut (aSource)
  {
    return this._composite.ArcLabelsOut(aSource);
  },
  
  GetAllResources: function nsExtensionsDataSource_GetAllResources ()
  {
    return this._composite.GetAllResources();
  },
  
  IsCommandEnabled: function nsExtensionsDataSource_IsCommandEnabled (aSources, aCommand, aArguments)
  {
    return this._composite.IsCommandEnabled(aSources, aCommand, aArguments);
  },
  
  DoCommand: function nsExtensionsDataSource_DoCommand (aSources, aCommand, aArguments)
  {
    this._composite.DoCommand(aSources, aCommand, aArguments);
  },
  
  GetAllCmds: function nsExtensionsDataSource_GetAllCmds (aSource)
  {
    return this._composite.GetAllCmds(aSource);
  },
  
  hasArcIn: function nsExtensionsDataSource_hasArcIn (aNode, aArc)
  {
    return this._composite.hasArcIn(aNode, aArc);
  },
  
  hasArcOut: function nsExtensionsDataSource_hasArcOut (aSource, aArc)
  {
    return this._composite.hasArcOut(aSource, aArc);
  },
  
  beginUpdateBatch: function nsExtensionsDataSource_beginUpdateBatch ()
  {
    return this._composite.beginUpdateBatch();
  },
  
  endUpdateBatch: function nsExtensionsDataSource_endUpdateBatch ()
  {
    return this._composite.endUpdateBatch();
  },
  
  /////////////////////////////////////////////////////////////////////////////
  // nsIRDFRemoteDataSource
  
  get loaded()
  {
    throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
  },
  
  Init: function nsExtensionsDataSource_Init (aURI)
  {
  },
  
  Refresh: function nsExtensionsDataSource_Refresh (aBlocking)
  {
  },
  
  Flush: function nsExtensionsDataSource_Flush ()
  {
    this._flush(false);
    this._flush(true);
  },
  
  FlushTo: function nsExtensionsDataSource_FlushTo (aURI)
  {
  },
  
  _flush: function nsExtensionsDataSource__flush (aIsProfile)
  { 
    var ds = aIsProfile ? this._profileExtensions : this._appExtensions;
    var rds = ds.QueryInterface(Components.interfaces.nsIRDFRemoteDataSource);
    rds.Flush();
  },

  /////////////////////////////////////////////////////////////////////////////
  // nsISupports
  QueryInterface: function nsExtensionsDataSource_QueryInterface (aIID) 
  {
    if (!aIID.equals(Components.interfaces.nsIRDFDataSource) &&
        !aIID.equals(Components.interfaces.nsIRDFRemoteDataSource) && 
        !aIID.equals(Components.interfaces.nsISupports))
      throw Components.results.NS_ERROR_NO_INTERFACE;
    return this;
  }
};


var gModule = {
  _firstTime: true,
  
  registerSelf: function (aComponentManager, aFileSpec, aLocation, aType) 
  {
    if (this._firstTime) {
      this._firstTime = false;
      throw Components.results.NS_ERROR_FACTORY_REGISTER_AGAIN;
    }
    aComponentManager = aComponentManager.QueryInterface(Components.interfaces.nsIComponentRegistrar);
    
    for (var key in this._objects) {
      var obj = this._objects[key];
      aComponentManager.registerFactoryLocation(obj.CID, obj.className, obj.contractID,
                                                aFileSpec, aLocation, aType);
    }

/*    
    // Make the Extension Manager a startup observer
    var categoryManager = Components.classes["@mozilla.org/categorymanager;1"]
                                    .getService(Components.interfaces.nsICategoryManager);
    categoryManager.addCategoryEntry("app-startup", this._objects.manager.className,
                                     "service," + this._objects.manager.contractID, 
                                     true, true, null);
 */
  },
  
  getClassObject: function (aComponentManager, aCID, aIID) 
  {
    if (!aIID.equals(Components.interfaces.nsIFactory))
      throw Components.results.NS_ERROR_NOT_IMPLEMENTED;

    for (var key in this._objects) {
      if (aCID.equals(this._objects[key].CID))
        return this._objects[key].factory;
    }
    
    throw Components.results.NS_ERROR_NO_INTERFACE;
  },
  
  _objects: {
    manager: { CID        : nsExtensionManager.prototype.classID,
               contractID : nsExtensionManager.prototype.contractID,
               className  : nsExtensionManager.prototype.classDescription,
               factory    : {
                              createInstance: function (aOuter, aIID) 
                              {
                                if (aOuter != null)
                                  throw Components.results.NS_ERROR_NO_AGGREGATION;
                                
                                return (new nsExtensionManager()).QueryInterface(aIID);
                              }
                            }
             }
   },
  
  canUnload: function (aComponentManager) 
  {
    return true;
  }
};

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