/* -*- 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>
 *
 * 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_ID              = "app.id";
const PREF_EM_APP_VERSION         = "app.version";
const PREF_EM_LAST_APP_VERSION    = "extensions.lastAppVersion";
const PREF_UPDATE_COUNT           = "update.extensions.count";
const PREF_UPDATE_EXT_WSDL_URI    = "update.extensions.wsdl";
const PREF_EM_WASINSAFEMODE       = "extensions.wasInSafeMode";

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 FILE_EXTENSIONS         = "Extensions.rdf";
const FILE_UNINSTALL_LOG      = "Uninstall";
const FILE_DEFAULTS           = "Defaults";
const FILE_AUTOREG            = ".autoreg";
const FILE_EXTENSION_MANIFEST = "extension.rdf";
const FILE_WASINSAFEMODE      = "Safe Mode";

///////////////////////////////////////////////////////////////////////////////
//
// Utility Functions
//
function EM_NS(aProperty)
{
  return "http://www.mozilla.org/2004/em-rdf#" + aProperty;
}

// Returns the specified directory hierarchy under the special directory 
// specified by aKey, creating directories along the way if necessary.
function getDir(aKey, aSubDirs)
{
  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 (!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 ? "ProfD" : "XCurProcD";
}

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

///////////////////////////////////////////////////////////////////////////////
//
// 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"
};

///////////////////////////////////////////////////////////////////////////////
//
// 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 ()
  {
    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, 0664, 0);
  },
  
  close: function ()
  {
    this._fos.close();  
  },
  
  addFile: function (aFile) 
  {
    var line = "add\t" + aFile.persistentDescriptor + "\n";
    this._fos.write(line, line.length);
  },
  
  registerChrome: function (aProviderName, aFileURL, 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);
  }  
};

///////////////////////////////////////////////////////////////////////////////
//
// 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 ()
  {
    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    
    for (var i = 0; i < lines.length; ++i)
      this._parseLine(lines[i]);
  },
  
  _parseLine: function (aLine)
  {
    var parts = aLine.split("\t");
    if (parts[0] == 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);
      lf.persistentDescriptor = filePD;
      this._listener.onAddFile(lf);
    }
    else if (parts[0] == this.TOKEN_REGISTER_CHROME) {
      var isProfile = parts[1] == this.TOKEN_PROFILE;
      this._listener.onRegisterChrome(parts[3], lf, parts[2], isProfile);
    }
  }
};

///////////////////////////////////////////////////////////////////////////////
//
// nsExtensionInstaller
//
function nsExtensionInstaller (aExtensionDS)
{
  this._rdf = Components.classes["@mozilla.org/rdf/rdf-service;1"]
                        .getService(Components.interfaces.nsIRDFService);    
  this._extensionDS = aExtensionDS;

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

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

  // Extension metadata
  _extensionID      : null,
  _isProfile        : true,
  _extDirKey        : "ProfD",
  
  // Source and target datasources
  _metadataDS       : null,
  _extensionDS      : null,
  
  // RDF objects and properties
  _provTypePackage  : null,
  _provTypeSkin     : null,
  _provTypeLocale   : null,
  _sourceResource   : null,
  _fileProperty     : null,
  
  install: function (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. 
    this._installExtensionFiles();
    
    // Load the metadata datasource
    var metadataFile = getFile(this._extDirKey, 
                               [DIR_EXTENSIONS, aExtensionID, FILE_EXTENSION_MANIFEST]);
    
    var ioServ = Components.classes["@mozilla.org/network/io-service;1"]
                           .getService(Components.interfaces.nsIIOService);
    var fph = ioServ.getProtocolHandler("file").QueryInterface(Components.interfaces.nsIFileProtocolHandler);
    this._metadataDS = this._rdf.GetDataSourceBlocking(fph.getURLSpecFromFile(metadataFile));
    
    // Add metadata for the extension to the global extension metadata set
    this._extensionDS.addExtensionMetaData(this._extensionID, this._metadataDS, this._isProfile);
    
    // Register chrome packages for files specified in the extension manifest
    this._registerChromeForExtension();
    this._writer.close();
    
    // Unset the "toBeInstalled" flag
    this._extensionDS.setExtensionProperty(this._extensionID, 
                                           this._extensionDS._emR("toBeInstalled"),
                                           null, this._isProfile);
  },
  
  _installExtensionFiles: function ()
  {
    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 ()
  {
    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 ()
  {
    // 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 (aFile, aChromeType, aPath)
  {
    var ioServ = Components.classes["@mozilla.org/network/io-service;1"]
                           .getService(Components.interfaces.nsIIOService);
    var fph = ioServ.getProtocolHandler("file").QueryInterface(Components.interfaces.nsIFileProtocolHandler);
    var fileURL = fph.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)) {
      cr.installPackage(fileURL, this._isProfile);
      type = this._writer.CHROME_TYPE_PACKAGE;
    }
    else if (aChromeType.EqualsNode(this._provTypeSkin)) {
      cr.installSkin(fileURL, this._isProfile, true); // Extension skins can execute scripts
      type = this._writer.CHROME_TYPE_SKIN;
    }
    else if (aChromeType.EqualsNode(this._provTypeLocale)) {
      cr.installLocale(fileURL, this._isProfile);
      type = this._writer.CHROME_TYPE_LOCALE;
    }
    var providerName = this._getProviderName(aPath);
    this._writer.registerChrome(providerName, fileURL, type, this._isProfile);
  },
  
  _getProviderName: function (aPath)
  {
    var parts = aPath.split("/");
    
    for (var i = 1; i < parts.length; ++i) {
      var lastPath = parts[parts.length - i];
      if (lastPath == "")
        continue;
      return lastPath;
    }
    return lastPath;
  }
};

///////////////////////////////////////////////////////////////////////////////
//
// 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 (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("******* EEEEEE = " + e + "\n");
    }
    
    // Unset the "toBeUninstalled" flag
    this._extensionDS.setExtensionProperty(this._extensionID, 
                                           this._extensionDS._emR("toBeUninstalled"),
                                           null, this._isProfile);
  },
  
  ///////////////////////////////////////////////////////////////////////////////
  // nsIInstallLogReaderListener
  onAddFile: function (aFile)
  {
    this._removeFile(aFile);
  },
  
  _removeFile: function (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);
    }
  },
  
  onRegisterChrome: function (aProviderName, aFile, aChromeType, aIsProfile)
  {
    switch (aChromeType) {
    case this._reader.CHROME_TYPE_PACKAGE:
      this._cr.uninstallPackage(aProviderName, aIsProfile)
      break;
    case this._reader.CHROME_TYPE_SKIN:
      this._cr.uninstallSkin(aProviderName, aIsProfile)
      break;
    case this._reader.CHROME_TYPE_LOCALE:
      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 (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 (aProviderName, aFile, aChromeType, aIsProfile)
  {
    if (aChromeType == this._reader.CHROME_TYPE_PACKAGE)
      this._cr.setAllowOverlaysForPackage(aProviderName, this._enable);
  },
  
  onAddFile: function (aFile)
  {
  }
};

///////////////////////////////////////////////////////////////////////////////
//
// nsExtensionManager
//
function nsExtensionManager()
{
  var os = Components.classes["@mozilla.org/observer-service;1"]
                     .getService(Components.interfaces.nsIObserverService);
  os.addObserver(this, "profile-after-change", false);
  os.addObserver(this, "profile-do-change", false);
}

nsExtensionManager.prototype = {
  _extInstaller     : null,
  _extUninstaller   : null,
  _extEnabler       : null,
  
  /////////////////////////////////////////////////////////////////////////////
  // nsIObserver
  observe: function (aSubject, aTopic, aData)
  {
    switch (aTopic) {
    case "profile-after-change":
      var os = Components.classes["@mozilla.org/observer-service;1"]
                         .getService(Components.interfaces.nsIObserverService);
      os.removeObserver(this, "profile-after-change");
      
      // XXXben this can go away once appstartup/runner code calls us. 
      this.checkForMismatches();
      break;
    case "profile-do-change":
      var os = Components.classes["@mozilla.org/observer-service;1"]
                         .getService(Components.interfaces.nsIObserverService);
      os.removeObserver(this, "profile-do-change");
      
      // 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 cmdLineSvc = Components.classes["@mozilla.org/appshell/commandLineService;1"]
                                 .getService(Components.interfaces.nsICmdLineService);
      var safeMode = cmdLineSvc.getCmdLineValue("-no-extensions") != null;
      if (!safeMode) {
        var pref = Components.classes["@mozilla.org/preferences-service;1"]
                             .getService(Components.interfaces.nsIPrefBranch);
        var wasInSafeModeFile = getFile("ProfD", [DIR_EXTENSIONS, FILE_WASINSAFEMODE]);
        if (wasInSafeModeFile.exists()) {
          var win = this._showProgressWindow();
          this._ensureDS();
          
          // Walk the list of extensions and re-activate overlays for packages 
          // that aren't disabled.
          var items = this._ds.getItemsWithFlagUnset("disabled");
          for (var i = 0; i < items.length; ++i)
            this._finalizeEnableDisable(items[i], false);
            
          wasInSafeModeFile.remove(false);
          
          win.close();
        }
        
        this._checkForGlobalInstalls(cmdLineSvc.getCmdLineValue("-install-global"));

        this._finishOperations();
        this._loadDefaults();      
      }
      else {
        var win = this._showProgressWindow();
      
        this._ensureDS();
        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;

        var wasInSafeModeFile = getFile("ProfD", [DIR_EXTENSIONS, FILE_WASINSAFEMODE]);
        if (!wasInSafeModeFile.exists())
          wasInSafeModeFile.create(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0644);
          
        win.close();
      }
    }
  },
  
  _checkForGlobalInstalls: function (aPath)
  {
    if (!aPath)
      return;
      
    // 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("XCurProcD", []);
      try {
        file.append(aPath);
      }
      catch (e) { /* can't handle this */ }
    }
    
    if (file.exists())
      this.installExtensionFromXPI(file, nsIExtensionManager.FLAG_INSTALL_GLOBAL);
    else {
      printf("Invalid XPI Path: " + aPath + "\n");
    }
  },
  
  _finishOperations: function ()
  {
    var profileAutoReg = getFile("ProfD", [FILE_AUTOREG]);
    var globalAutoReg = getFile("XCurProcD", [FILE_AUTOREG]);
    if (profileAutoReg.exists() || globalAutoReg.exists()) {
      var win = this._showProgressWindow();
    
      // 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]);
      
      // 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
      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;
        
      win.close();
      
      // Now that we've finalized the EM operation, remove the autoreg files so
      // we don't do this every time we start.
      if (profileAutoReg.exists()) 
        profileAutoReg.remove(false);
      if (globalAutoReg.exists())
        globalAutoReg.remove(false);
    }
  },
  
  // 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 ()
  {
    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 ()
  {
    // Load default preferences files for all extensions
    var defaultsManifest = getFile("ProfD", 
                                   [DIR_EXTENSIONS, FILE_DEFAULTS]);
    if (defaultsManifest.exists()) {
      var pref = Components.classes["@mozilla.org/preferences-service;1"]
                          .getService(Components.interfaces.nsIPrefService);
      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())
            pref.readUserPrefs(lf);
        }
      }
      while (more);
      fis.close();
    }
  },
  
  checkForMismatches: function () 
  {
    var rv = false;
    
    // Check to see if the version of the application that is being started
    // now is the same one that was started last time. 
    var pref = Components.classes["@mozilla.org/preferences-service;1"]
                         .getService(Components.interfaces.nsIPrefBranch);
    var currAppVersion = pref.getCharPref(PREF_EM_APP_VERSION);
    try {
      var lastAppVersion = pref.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 = pref.getCharPref(PREF_EM_APP_ID);
      var items = this._ds.getIncompatibleItemList(currAppID, currAppVersion);

      if (items.length > 0) {
        for (var i = 0; i < items.length; ++i) {
          // Now disable the extension so it won't hurt anything. 
          this.disableExtension(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);
        
        rv = true;
      }
    }
    
    // Now update the last app version so we don't do this checking 
    // again.
    pref.setCharPref(PREF_EM_LAST_APP_VERSION, currAppVersion);
    
    return rv;
  },
  
  get inSafeMode() 
  {
    return this._ds.safeMode;
  },
  
  _writeProfileFile: function (aFile, aGetDirFunc, aWriteLineFunc)
  {
    // 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, 0664, 0);

    var extensions = this.getItemList(null, nsIUpdateItem.TYPE_EXTENSION, { });
    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.getExtensionProperty(extension.id, "toBeDisabled");
      var toBeUninstalled = this._ds.getExtensionProperty(extension.id, "toBeUninstalled");
      var disabled = this._ds.getExtensionProperty(extension.id, "disabled");
      if (toBeDisabled == "true" || toBeUninstalled == "true" || disabled == "true")
        continue;
      
      var sourceDir = aGetDirFunc(this._ds.isProfileExtension(extension.id), 
                                  extension.id);
      if (sourceDir.exists())
        aWriteLineFunc(fos, sourceDir, extension.id);
    }
    fos.close();
  },
  
  _getComponentsDir: function (aIsProfile, aExtensionID)
  {
    return getDir(getDirKey(aIsProfile), 
                  [DIR_EXTENSIONS, aExtensionID, DIR_COMPONENTS]);
  },
  
  _getPreferencesDir: function (aIsProfile, aExtensionID)
  {
    return getDir(getDirKey(aIsProfile), 
                  [DIR_EXTENSIONS, aExtensionID, 
                   DIR_DEFAULTS, DIR_DEFAULTS_PREFS]);
  },
  
  _writeAutoregLines: function (aStream, aSourceDir, aExtensionID)
  {
    var line = "reg:extensions/" + aExtensionID + "/components/\n";
    aStream.write(line, line.length);
  },
  
  _writePreferencesLines: function (aStream, aSourceDir, aExtensionID)
  {
    var files = aSourceDir.directoryEntries;
    while (files.hasMoreElements()) {
      var file = files.getNext().QueryInterface(Components.interfaces.nsILocalFile);
      var line = file.path + "\n";
      aStream.write(line, line.length);
    }
  },

  _writeAutoReg: function (aIsProfile)
  {
    var autoRegFile = aIsProfile ? getFile("ProfD", [FILE_AUTOREG]) : 
                                   getFile("XCurProcD", [FILE_AUTOREG]);
    this._writeProfileFile(autoRegFile, 
                           this._getComponentsDir, 
                           this._writeAutoregLines);
  },
  
  _writeDefaults: function ()
  {
    this._writeProfileFile(getFile("ProfD", [DIR_EXTENSIONS, FILE_DEFAULTS]), 
                           this._getPreferencesDir, 
                           this._writePreferencesLines);
  },
  
  _cleanDirs: function ()
  {
    var keys = ["ProfD", "XCurProcD"];
    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())
          entry.remove(false);
      }
    }
  },
  
  /////////////////////////////////////////////////////////////////////////////  
  // nsIExtensionManager
  installExtensionFromXPI: function (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 zipReader = Components.classes["@mozilla.org/libjar/zip-reader;1"]
                              .createInstance(Components.interfaces.nsIZipReader);
    zipReader.init(aXPIFile);
    zipReader.open();

    var installProfile = aFlags & nsIExtensionManager.FLAG_INSTALL_PROFILE;
    var tempManifest = getFile(getDirKey(installProfile),
                               [DIR_EXTENSIONS, DIR_TEMP, FILE_EXTENSION_MANIFEST]);
    zipReader.extract(FILE_EXTENSION_MANIFEST, tempManifest);
    
    var rdf = Components.classes["@mozilla.org/rdf/rdf-service;1"]
                        .getService(Components.interfaces.nsIRDFService);
    var ioServ = Components.classes["@mozilla.org/network/io-service;1"]
                           .getService(Components.interfaces.nsIIOService);
    var fph = ioServ.getProtocolHandler("file").QueryInterface(Components.interfaces.nsIFileProtocolHandler);
    var ds = rdf.GetDataSourceBlocking(fph.getURLSpecFromFile(tempManifest));
    
    // 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._canInstallExtension(ds);
    if (extensionID) {
      this._ds.setExtensionProperty(extensionID, this._ds._emR("toBeInstalled"),
                                    this._ds._emL("true"), installProfile);
      // 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);
    }
    zipReader.close();
    tempManifest.remove(false);
    
    this._writeAutoReg(installProfile);
  },
  
  _canInstallExtension: function (aDataSource)
  {
    var rdf = Components.classes["@mozilla.org/rdf/rdf-service;1"]
                        .getService(Components.interfaces.nsIRDFService);
    var manifestRoot = rdf.GetResource("urn:mozilla:extension:manifest");
    // Check the target application range specified by the extension metadata.
    if (this._ds.isCompatible(aDataSource, manifestRoot)) {
      var id = rdf.GetResource(EM_NS("id"));
      var idLiteral = aDataSource.GetTarget(manifestRoot, id, true);
      return idLiteral.QueryInterface(Components.interfaces.nsIRDFLiteral).Value;
    }
    return null;
  },
  
  _stageExtensionXPI: function (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);
  },
  
  // This function is called on the next startup 
  _finalizeInstall: function (aExtensionID)
  {
    if (!this._extInstaller)
      this._extInstaller = new nsExtensionInstaller(this._ds);
    this._extInstaller.install(aExtensionID, 
                               this._ds.isProfileExtension(aExtensionID));
    
    // Update the Defaults Manifest
    this._writeDefaults();
  },
  
  _finalizeEnableDisable: function (aExtensionID, aDisable)
  {
    if (!this._extEnabler)
      this._extEnabler = new nsExtensionEnabler(this._ds);
    this._extEnabler.enable(aExtensionID, 
                            this._ds.isProfileExtension(aExtensionID), 
                            aDisable);
    this._writeDefaults();
  },
  
  _finalizeUninstall: function (aExtensionID)
  {
    if (!this._extUninstaller)
      this._extUninstaller = new nsExtensionUninstaller(this._ds);
    this._extUninstaller.uninstall(aExtensionID, 
                                   this._ds.isProfileExtension(aExtensionID));
    
    // Update the Defaults Manifest
    this._writeDefaults();
  },
      
  uninstallExtension: function (aExtensionID)
  {
    this._ds.uninstallExtension(aExtensionID);
    this._writeAutoReg(this._ds.isProfileExtension(aExtensionID));
  },
  
  enableExtension: function (aExtensionID)
  {
    this._ds.enableExtension(aExtensionID);
    this._writeAutoReg(this._ds.isProfileExtension(aExtensionID));
  },
  
  disableExtension: function (aExtensionID)
  {
    this._ds.disableExtension(aExtensionID);
    this._writeAutoReg(this._ds.isProfileExtension(aExtensionID));
  },
  
  update: function (aItems, aItemCount)
  {
    var pref = Components.classes["@mozilla.org/preferences-service;1"]
                         .getService(Components.interfaces.nsIPrefBranch);
    var appID = pref.getCharPref(PREF_EM_APP_ID);
    var appVersion = pref.getCharPref(PREF_EM_APP_VERSION);

    if (aItems.length == 0) {
      var anyType = nsIUpdateItem.TYPE_ANY;
      aItems = this.getItemList(null, anyType, { });
    }
    var updater = new nsExtensionItemUpdater(aItems, appID, appVersion);
    updater.checkForUpdates();
  },
  
  getItemList: function (aItemID, aType, aCountRef)
  {
    this._ensureDS();
    return this._ds.getItemList(aItemID, aType, aCountRef);
  },    
  
  // Themes
  installTheme: function (aThemeID)
  {
  
  },
  
  uninstallTheme: function (aThemeID)
  {
  
  },

  get datasource()
  {
    this._ensureDS();
    return this._ds;
  },
  
  //
  _ds: null,

  /////////////////////////////////////////////////////////////////////////////    
  // Other
  
  // This should NOT be called until after the window is shown! 
  _ensureDS: function ()
  {
    if (!this._ds) {
      dump("*** loading the extensions datasource\n");
      this._ds = new nsExtensionsDataSource();
      if (this._ds) {
        this._ds.loadExtensions(false);
        this._ds.loadExtensions(true);
      }
    }
  },

  /////////////////////////////////////////////////////////////////////////////
  // nsIClassInfo
  getInterfaces: function (aCount)
  {
    var interfaces = [Components.interfaces.nsIExtensionManager,
                      Components.interfaces.nsIObserver];
    aCount.value = interfaces.length;
    return interfaces;
  },
  
  getHelperForLanguage: function (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 (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;
  }
};

///////////////////////////////////////////////////////////////////////////////
//
// nsExtensionItemUpdater
//
function nsExtensionItemUpdater(aItems, aTargetAppID, aTargetAppVersion) 
{
  this._items = aItems;
  this._count = aItems.length;
  this._appID = aTargetAppID;
  this._appVersion = aTargetAppVersion;

  this._os = Components.classes["@mozilla.org/observer-service;1"]
                       .getService(Components.interfaces.nsIObserverService);

  // This is the number of extensions/themes/etc that we found updates for.
  this._updateCount = 0;
}

nsExtensionItemUpdater.prototype = {
  /////////////////////////////////////////////////////////////////////////////
  // nsIExtensionItemUpdater
  checkForUpdates: function () 
  {
    this._os.notifyObservers(null, "Update:Extension:Started", "");
    var wspFactory = Components.classes["@mozilla.org/xmlextras/proxy/webserviceproxyfactory;1"]
                               .getService(Components.interfaces.nsIWebServiceProxyFactory);
    var pref = Components.classes["@mozilla.org/preferences-service;1"]
                         .getService(Components.interfaces.nsIPrefBranch);
    var wsdlURI = pref.getComplexValue(PREF_UPDATE_EXT_WSDL_URI,
                                       Components.interfaces.nsIPrefLocalizedString).data;
    wspFactory.createProxyAsync(wsdlURI, "VersionCheck", "", true, this);
    
    for (var i = 0; i < this._items.length; ++i) {
      var e = this._items[i];
      if (e.updateRDF) {
        var dsURI = e.updateRDF;
        dsURI = dsURI.replace(/%ITEM_ID%/g, e.id);
        dsURI = dsURI.replace(/%ITEM_VERSION%/g, e.version);
        dsURI = dsURI.replace(/%APP_ID%/g, this._appID);
        dsURI = dsURI.replace(/%APP_VERSION%/g, this._appVersion);
        var rdf = Components.classes["@mozilla.org/rdf/rdf-service;1"]
                            .getService(Components.interfaces.nsIRDFService);
        var ds = rdf.GetDataSource(dsURI);
        var rds = ds.QueryInterface(Components.interfaces.nsIRDFRemoteDataSource)
        if (rds.loaded)
          this.onDatasourceLoaded(ds);
        else {
          var sink = ds.QueryInterface(Components.interfaces.nsIRDFXMLSink);
          sink.addXMLSinkObserver(new nsExtensionUpdateXMLRDFDSObserver(this, e));
        }
      }
    }
  },
  
  /////////////////////////////////////////////////////////////////////////////
  // nsExtensionItemUpdater
  _proxy: null,
  
  _checkForUpdates: function ()
  {
    for (var i = 0; i < this._items.length; ++i) {
      var e = this._items[i];
      if (!e.updateRDF) {
        this._os.notifyObservers(null, "Update:Extension:Item-Started", e.name + " " + e.version);
        this._proxy.getNewestExtension(eval(e.objectSource), this._appID, this._appVersion);
      }
    }
  },

  /////////////////////////////////////////////////////////////////////////////
  // nsIWSDLLoadListener  
  onLoad: function (aProxy)
  { 
    this._proxy = aProxy;
    this._proxy.setListener(this);
    this._checkForUpdates();
  },
  
  onError: function (aStatus, aMessage)
  {
    this._os.notifyObservers(null, "Update:Extension:Item-Error", aMessage);
  },
  
  onDatasourceLoaded: function (aDatasource, aExtension)
  {
    ///////////////////////////////////////////////////////////////////////////    
    // The extension update RDF file looks something like this:
    // <RDF:Description about="urn:mozilla:extension:{EXTENSION GUID}">
    //  <em:version>5.0</em:version>
    //  <em:updateLink><XPI URL></em:updateLink>
    // </RDF:Description>

    // Parse the response RDF
    var rdf = Components.classes["@mozilla.org/rdf/rdf-service;1"]
                        .getService(Components.interfaces.nsIRDFService);
    var versionArc = rdf.GetResource(EM_NS("version"));
    var updateLinkArc = rdf.GetResource(EM_NS("updateLink"));
    var extensionRes = rdf.GetResource("urn:mozilla:extension:" + aExtension.id);
    
    var version = aDatasource.GetTarget(extensionRes, versionArc, true);
    version = version.QueryInterface(Components.interfaces.nsIRDFLiteral).Value;
    var updateLink = aDatasource.GetTarget(extensionRes, updateLinkArc, true);
    updateLink = updateLink.QueryInterface(Components.interfaces.nsIRDFLiteral).Value;
    
    // Check to see if this is a newer version.
    var versionChecker = Components.classes["@mozilla.org/updates/version-checker;1"]
                                   .getService(Components.interfaces.nsIVersionChecker);
    if (versionChecker.compare(aExtension.version, version) < 0) {
      // Construct an update item and pass it to observers. 
      var item = Components.classes["@mozilla.org/updates/item;1"]
                           .createInstance(Components.interfaces.nsIUpdateItem);
      item.init(aExtension.id, version, aExtension.name, -1, updateLink, "", "", 
                Components.interfaces.TYPE_EXTENSION); // XXXben
      this._os.notifyObservers(item, "Update:Extension:Item-Ended", "");
      ++this._updateCount;
    }

    this._checkForDone();
  },
  
  onDatasourceError: function (aExtension)
  {
    // XXXben - some way of indicating an error in the update process
    this._checkForDone();
  },
  
  getNewestExtensionCallback: function (aResult)
  {
    try {
      aResult.name.toString(); // XXXben This is a lame hack to cause an exception to be
                               // thrown for null values when there is no newer extension
                               // or something else bad happens on the server that we 
                               // don't recognize.
      var item = Components.classes["@mozilla.org/updates/item;1"]
                           .createInstance(Components.interfaces.nsIUpdateItem);
      item.init(aResult.id, aResult.version, aResult.name, -1, 
                aResult.updateURL, aResult.iconURL, "", 
                aResult.type); 
      this._os.notifyObservers(item, "Update:Extension:Item-Ended", "");
      ++this._updateCount;
    }
    catch (e) {
    }
    
    this._checkForDone();
  },
  
  _checkForDone: function ()
  {
    if (--this._count == 0) {
      var pref = Components.classes["@mozilla.org/preferences-service;1"]
                           .getService(Components.interfaces.nsIPrefBranch);
      pref.setIntPref(PREF_UPDATE_COUNT, this._updateCount); 
    
      this._os.notifyObservers(null, "Update:Extension:Ended", "");
    }
  },
  
  /////////////////////////////////////////////////////////////////////////////
  // nsISupports
  QueryInterface: function (aIID) 
  {
    if (!aIID.equals(Components.interfaces.nsIExtensionItemUpdater) &&
        !aIID.equals(Components.interfaces.nsISupports))
      throw Components.results.NS_ERROR_NO_INTERFACE;
    return this;
  }  
};


function nsExtensionUpdateXMLRDFDSObserver(aUpdater, aExtension)
{
  this._updater   = aUpdater;
  this._extension = aExtension;
}

nsExtensionUpdateXMLRDFDSObserver.prototype = 
{ 
  _updater    : null,
  _extension  : null,
  
  /////////////////////////////////////////////////////////////////////////////
  // nsIRDFXMLSinkObserver
  onBeginLoad: function(aSink)
  {
  },
  onInterrupt: function(aSink)
  {
  },
  onResume: function(aSink)
  {
  },
  
  onEndLoad: function(aSink)
  {
    aSink.removeXMLSinkObserver(this);
    
    var ds = aSink.QueryInterface(Components.interfaces.nsIRDFDataSource);
    this._updater.onDatasourceLoaded(ds, this._extension);
  },
  
  onError: function(aSink, aStatus, aErrorMsg)
  {
    aSink.removeXMLSinkObserver(this);
    
    this._updater.onDatasourceError(this._extension);
  }
};

///////////////////////////////////////////////////////////////////////////////
//
// nsExtensionsDataSource
//
function nsExtensionsDataSource()
{
  this._rdf = Components.classes["@mozilla.org/rdf/rdf-service;1"]
                        .getService(Components.interfaces.nsIRDFService);
}

nsExtensionsDataSource.prototype = {
  _rdf                : null,
  _appExtensions      : null,
  _profileExtensions  : null,  
  _composite          : null,
  safeMode            : false,
  
  _emR: function (aProperty) 
  {
    return this._rdf.GetResource(EM_NS(aProperty));
  },
  
  _emL: function (aLiteral)
  {
    return this._rdf.GetLiteral(aLiteral);
  },
  
  _rootRes: function (aRoot, aRoot)
  {
    return this._rdf.GetResource(aRoot + aID);
  },

  _stripPrefix: function (aResourceURI)
  {
    return aResourceURI.substr("urn:mozilla:extension:".length, aResourceURI.length);
  },
  
  isCompatible: function (aDS, aSource)
  {
    var pref = Components.classes["@mozilla.org/preferences-service;1"]
                         .getService(Components.interfaces.nsIPrefBranch);
    var appVersion = pref.getCharPref(PREF_EM_APP_VERSION);
    var appID = pref.getCharPref(PREF_EM_APP_ID);

    var targets = aDS.GetTargets(aSource, this._emR("targetApplication"), true);
    while (targets.hasMoreElements()) {
      var targetApp = targets.getNext().QueryInterface(Components.interfaces.nsIRDFLiteral).Value;

      var versionParts = targetApp.split(",");
      if (versionParts[0] == appID) {
        var versionChecker = Components.classes["@mozilla.org/updates/version-checker;1"]
                                       .getService(Components.interfaces.nsIVersionChecker);
        return ((versionChecker.compare(appVersion, versionParts[1]) >= 0) &&
                (versionChecker.compare(appVersion, versionParts[2]) <= 0));
      }
    }
    return false;
  },

  getIncompatibleItemList: function (aAppID, aAppVersion)
  {
    var items = [];
    
    var ctr = Components.classes["@mozilla.org/rdf/container;1"]
                        .createInstance(Components.interfaces.nsIRDFContainer);
    ctr.Init(this, this._rdf.GetResource("urn:mozilla:extension:root"));
    
    var elements = ctr.GetElements();
    while (elements.hasMoreElements()) {
      var e = elements.getNext().QueryInterface(Components.interfaces.nsIRDFResource);
      if (!this.isCompatible(this, e)) {
        var id = this._stripPrefix(e.Value);
        var item = Components.classes["@mozilla.org/updates/item;1"]
                             .createInstance(Components.interfaces.nsIUpdateItem);
        item.init(id, this.getExtensionProperty(id, "version"),
                  this.getExtensionProperty(id, "name"),
                  -1, "", "", this.getExtensionProperty(id, "updateURL"), 
                  nsIUpdateItem.TYPE_EXTENSION); // XXXben
        items.push(item);
      }
    }
    return items;
  },
  
  getItemList: function (aItemID, aType, aCountRef)
  {
    var items = [];
    if (aItemID) {
      var item = Components.classes["@mozilla.org/updates/item;1"]
                           .createInstance(Components.interfaces.nsIUpdateItem);
      item.init(aItemID, this.getExtensionProperty(aItemID, "version"),
                this.getExtensionProperty(aItemID, "name"),
                -1, "", "", this.getExtensionProperty(aItemID, "updateURL"), 
                aType);
      items.push(item);
    }
    else {
      var ctr = Components.classes["@mozilla.org/rdf/container;1"]
                          .createInstance(Components.interfaces.nsIRDFContainer);
      ctr.Init(this, this._rdf.GetResource("urn:mozilla:extension:root"));
      
      var elements = ctr.GetElements();
      while (elements.hasMoreElements()) {
        var e = elements.getNext().QueryInterface(Components.interfaces.nsIRDFResource);
        var id = this._stripPrefix(e.Value);
        var item = Components.classes["@mozilla.org/updates/item;1"]
                             .createInstance(Components.interfaces.nsIUpdateItem);
        item.init(id, this.getExtensionProperty(id, "version"),
                  this.getExtensionProperty(id, "name"),
                  -1, "", "", 
                  this.getExtensionProperty(id, "updateURL"), aType);
        items.push(item);
      }
    }
    aCountRef.value = items.length;
    return items;
  },
  
  getItemsWithFlagSet: function (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(this._stripPrefix(e.Value));
    }
    return items;
  },
  
  getItemsWithFlagUnset: function (aFlag)
  {
    var items = [];
    
    var ctr = Components.classes["@mozilla.org/rdf/container;1"]
                        .createInstance(Components.interfaces.nsIRDFContainer);
    ctr.Init(this, this._rdf.GetResource("urn:mozilla:extension:root"));
    
    var elements = ctr.GetElements();
    while (elements.hasMoreElements()) {
      var e = elements.getNext().QueryInterface(Components.interfaces.nsIRDFResource);
      
      var value = this.GetTarget(e, this._emR(aFlag), true);
      if (!value)
        items.push(this._stripPrefix(e.Value));
    }
    return items;
  },
  
  isProfileExtension: function (aExtensionID)
  {
    return this.getExtensionProperty(aExtensionID, "installLocation") != "global";
  },
  
  _setProperty: function (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);
  },
  
  getExtensionProperty: function (aExtensionID, aProperty)
  {
    var extension = this._rdf.GetResource("urn:mozilla:extension:" + aExtensionID);
    try {
      return this.GetTarget(extension, this._emR(aProperty), true).QueryInterface(Components.interfaces.nsIRDFLiteral).Value;
    }
    catch (e) {}
    return "";
  },
  
  setExtensionProperty: function (aExtensionID, aPropertyArc, aPropertyValue, aIsProfile)
  {
    var extension = this._rdf.GetResource("urn:mozilla:extension:" + aExtensionID);
    if (aIsProfile === undefined)
      aIsProfile = this.isProfileExtension(aExtensionID);
    var ds = aIsProfile ? this._profileExtensions : this._appExtensions;
    
    this._setProperty(ds, extension, aPropertyArc, aPropertyValue);

    this._flush(aIsProfile);  
  },
  
  addExtensionMetaData: function (aExtensionID, aSourceDS, 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, this._rdf.GetResource("urn:mozilla:extension:root"));

    var targetRes = this._rdf.GetResource("urn:mozilla:extension:" + aExtensionID);
    // 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);
    
    // Copy the assertions over from the source datasource. 
    
    // Assert properties with single values
    var singleProps = ["version", "name", "description", "creator", "homepageURL", 
                       "updateURL", "optionsURL", "aboutURL", "iconURL"];
    var sourceRes = this._rdf.GetResource("urn:mozilla:extension: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 = ["targetApplication", "requires", "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);
      }
    }
    
    // Unset the "to be installed" flag since we're done installing now.
    // targetDS.Unassert(targetRes, this._emR("toBeInstalled"), this._emL("true"));
  },
  
  enableExtension: function (aExtensionID)
  {
    this.setExtensionProperty(aExtensionID, this._emR("toBeEnabled"), this._emL("true"));
    this.setExtensionProperty(aExtensionID, this._emR("toBeDisabled"), null);
    this.setExtensionProperty(aExtensionID, this._emR("disabled"), null);
  },
  
  disableExtension: function (aExtensionID)
  {
    this.setExtensionProperty(aExtensionID, this._emR("toBeDisabled"), this._emL("true"));
    this.setExtensionProperty(aExtensionID, this._emR("toBeEnabled"), null);
    this.setExtensionProperty(aExtensionID, this._emR("disabled"), this._emL("true"));
  },
  
  uninstallExtension: function (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.isProfileExtension(aExtensionID);
    
    var ctr = Components.classes["@mozilla.org/rdf/container;1"]
                        .createInstance(Components.interfaces.nsIRDFContainer);
    ctr.Init(this, this._rdf.GetResource("urn:mozilla:extension:root"));
    
    var extension = this._rdf.GetResource("urn:mozilla:extension:" + aExtensionID);
    ctr.RemoveElement(extension, true);
    
    // Clean the extension resource
    this._removeExtensionMetadata(aExtensionID);
    
    this.setExtensionProperty(aExtensionID, this._emR("toBeUninstalled"), 
                              this._emL("true"), 
                              isProfile);
    this.Flush();
  },
  
  _removeExtensionMetadata: function (aExtensionID)
  {
    var extension = this._rdf.GetResource("urn:mozilla:extension:" + aExtensionID);
    var isProfile = this.isProfileExtension(aExtensionID);
    var ds = isProfile ? this._profileExtensions : this._appExtensions;

    // Remove outward arcs
    var arcs = ds.ArcLabelsOut(extension);
    while (arcs.hasMoreElements()) {
      var arc = arcs.getNext().QueryInterface(Components.interfaces.nsIRDFResource);
      var value = ds.GetTarget(extension, arc, true);
      if (value)
        ds.Unassert(extension, arc, value);
    }
  },
  
  loadExtensions: function (aProfile)
  {
    var fileLocator = Components.classes["@mozilla.org/file/directory_service;1"]
                                .getService(Components.interfaces.nsIProperties);
    var extensionsFile  = getFile(getDirKey(aProfile), 
                                  [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. 
    if (!extensionsFile.exists()) {
      var defaultFile = getFile("ProfDefNoLoc", 
                                ["extensions", FILE_EXTENSIONS]);
      defaultFile.copyTo(extensionsFile.parent, extensionsFile.leafName);
    }

    var ioServ = Components.classes["@mozilla.org/network/io-service;1"]
                           .getService(Components.interfaces.nsIIOService);
    var fph = ioServ.getProtocolHandler("file").QueryInterface(Components.interfaces.nsIFileProtocolHandler);
    var dsURL = fph.getURLSpecFromFile(extensionsFile);
    
    var ds = this._rdf.GetDataSourceBlocking(dsURL);
    
    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 (aProperty, aTarget, aTruthValue)
  {
    return this._composite.GetSource(aProperty, aTarget, aTruthValue);
  },
  
  GetSources: function (aProperty, aTarget, aTruthValue)
  {
    return this._composite.GetSources(aProperty, aTarget, aTruthValue);
  },
  
  GetTarget: function (aSource, aProperty, aTruthValue)
  {
    if (aProperty.EqualsNode(this._emR("iconURL"))) {
      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 this._rdf.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 this._rdf.GetResource("chrome://mozapps/skin/xpinstall/xpinstallItemGeneric.png");
        }
      }
    }
    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.
    }
    
    return this._composite.GetTarget(aSource, aProperty, aTruthValue);
  },
  
  GetTargets: function (aSource, aProperty, aTruthValue)
  {
    return this._composite.GetTargets(aSource, aProperty, aTruthValue);
  },
  
  _getTargetDSFromSource: function (aSource)
  {
    var extensionID = aSource.Value.substr("urn:mozilla:extension:".length, aSource.Value.length);
    return this.isProfileExtension(extensionID) ? this._profileExtensions : this._appExtensions;
  },
  
  Assert: function (aSource, aProperty, aTarget, aTruthValue)
  {
    var targetDS = this._getTargetDSFromSource(aSource);
    targetDS.Assert(aSource, aProperty, aTarget, aTruthValue);
  },
  
  Unassert: function (aSource, aProperty, aTarget)
  {
    var targetDS = this._getTargetDSFromSource(aSource);
    targetDS.Unassert(aSource, aProperty, aTarget);
  },
  
  Change: function (aSource, aProperty, aOldTarget, aNewTarget)
  {
    var targetDS = this._getTargetDSFromSource(aSource);
    targetDS.Change(aSource, aProperty, aOldTarget, aNewTarget);
  },

  Move: function (aSource, aNewSource, aProperty, aTarget)
  {
    var targetDS = this._getTargetDSFromSource(aSource);
    targetDS.Move(aSource, aNewSource, aProperty, aTarget);
  },
  
  HasAssertion: function (aSource, aProperty, aTarget, aTruthValue)
  {
    return this._composite.HasAssertion(aSource, aProperty, aTarget, aTruthValue);
  },
  
  AddObserver: function (aObserver)
  {
    this._composite.AddObserver(aObserver);
  },
  
  RemoveObserver: function (aObserver)
  {
    this._composite.RemoveObserver(aObserver);
  },
  
  ArcLabelsIn: function (aNode)
  {
    return this._composite.ArcLabelsIn(aNode);
  },
  
  ArcLabelsOut: function (aSource)
  {
    return this._composite.ArcLabelsOut(aSource);
  },
  
  GetAllResources: function ()
  {
    return this._composite.GetAllResources();
  },
  
  IsCommandEnabled: function (aSources, aCommand, aArguments)
  {
    return this._composite.IsCommandEnabled(aSources, aCommand, aArguments);
  },
  
  DoCommand: function (aSources, aCommand, aArguments)
  {
    this._composite.DoCommand(aSources, aCommand, aArguments);
  },
  
  GetAllCmds: function (aSource)
  {
    return this._composite.GetAllCmds(aSource);
  },
  
  hasArcIn: function (aNode, aArc)
  {
    return this._composite.hasArcIn(aNode, aArc);
  },
  
  hasArcOut: function (aSource, aArc)
  {
    return this._composite.hasArcOut(aSource, aArc);
  },
  
  beginUpdateBatch: function ()
  {
    return this._composite.beginUpdateBatch();
  },
  
  endUpdateBatch: function ()
  {
    return this._composite.endUpdateBatch();
  },
  
  /////////////////////////////////////////////////////////////////////////////
  // nsIRDFRemoteDataSource
  
  get loaded()
  {
    throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
  },
  
  Init: function (aURI)
  {
  },
  
  Refresh: function (aBlocking)
  {
  },
  
  Flush: function ()
  {
    this._flush(false);
    this._flush(true);
  },
  
  FlushTo: function (aURI)
  {
  },
  
  _flush: function (aIsProfile)
  { 
    var ds = aIsProfile ? this._profileExtensions : this._appExtensions;
    var rds = ds.QueryInterface(Components.interfaces.nsIRDFRemoteDataSource);
    rds.Flush();
  },

  /////////////////////////////////////////////////////////////////////////////
  // nsISupports
  QueryInterface: function (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;
}
