/* -*- 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 ***** */

var gExtensionManager = null;

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

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

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

function nsInstallLogger(aExtensionID, aIsProfile)
{
  this._isProfile = aIsProfile; // XXXben
  this._extensionDir = getDir(aIsProfile ? "ProfD" : "ProfD", ["extensions", aExtensionID]); // XXXben XCurProcDir
  this._uninstallDir = this._extensionDir.clone();
  this._uninstallDir.append("uninstall");
  var uninstallLog = this._uninstallDir.clone();
  uninstallLog.append("uninstall.rdf");
  var ioServ = Components.classes["@mozilla.org/network/io-service;1"]
                         .getService(Components.interfaces.nsIIOService);
  var fph = ioServ.getProtocolHandler("file").QueryInterface(Components.interfaces.nsIFileProtocolHandler);
  var rdf = Components.classes["@mozilla.org/rdf/rdf-service;1"]
                      .getService(Components.interfaces.nsIRDFService);
  this._ds = rdf.GetDataSourceBlocking(fph.getURLSpecFromFile(uninstallLog));
}

nsInstallLogger.prototype = {
  _extensionDir: null,
  _uninstallDir: null,
  _ds: null,
  
  addFile: function (aFile) 
  {
    dump("*** addFile: " + aFile.path + "\n");
  },
  
  replaceFile: function (aFile)
  {
  
  },
  
  removeFile: function (aFile)
  {
  
  }
};

function VersionChecker(aExtensionResource, aAppID, aAppVersion, aDataSource)
{
  this._extensionResource = aExtensionResource;
  this._appID = aAppID;
  this._appVersion = aAppVersion;
  this._ds = aDataSource;
}

VersionChecker.prototype = { 
  get isCompatible ()
  {
    var targets = this._ds.GetTargets(this._extensionResource, this._ds._emR("targetApplication"), true);
    while (targets.hasMoreElements()) {
      var targetAppString = targets.getNext().QueryInterface(Components.interfaces.nsIRDFLiteral);

      var versionParts = targetAppString.Value.split(",");
      if (versionParts[0] == this._appID) {
        var minRequiredVersionStr = versionParts[1];
        var maxRequiredVersionStr = versionParts[2];
        var power = this._getLargestPower([this._appVersion, 
                                          minRequiredVersionStr, 
                                          maxRequiredVersionStr]);
        var minRequiredVersion = this._parseVersion(minRequiredVersionStr, power);
        var maxRequiredVersion = this._parseVersion(maxRequiredVersionStr, power);
        var appVersion = this._parseVersion(this._appVersion, power);
        
        return (appVersion >= minRequiredVersion && 
                appVersion <= maxRequiredVersion);
      }
    }
    return false;
  },

  // Convert a version string into an integer value
  _parseVersion: function (aVersion, aPower)
  {
    var parts = aVersion.split(".");
    var version = 0;
    if (aPower == 0)
      aPower = parts.length;
    
    for (var i = 0; i < parts.length; ++i) {
      var token = parts[i];
      if (token.charAt(token.length-1) == "+") {
        token = token.substr(0, token.lastIndexOf("+"));
        version += 1;
        if (token.length == 0)
          continue;
      }
      
      version += parseInt(token) * Math.pow(10, aPower - i);
    }
    return version;
  },
  
  _parsePower: function (aVersion)
  {
    return aVersion.split(".").length;
  },

  _getLargestPower: function (aVersionArray)
  {
    var biggestPower = 0;
    for (var i = 0; i < aVersionArray.length; ++i) {
      var power = this._parsePower(aVersionArray[i]);
      if (power > biggestPower) 
        biggestPower = power;
    }
    return biggestPower;
  }
}

function nsJarFileExtractor(aXPIFile, aTargetDir)
{
  this._xpiFile = aXPIFile.path;
  this._targetDir = aTargetDir.path;
  // this._proxyObject(Components, Components.interfaces.nsIXPCComponents, "_components");
  /* 
  this._proxyObject(aXPIFile,   Components.interfaces.nsIFile, "_xpiFile");
  this._proxyObject(aTargetDir, Components.interfaces.nsIFile, "_targetDir");
   */
}

nsJarFileExtractor.prototype = {
  // proxied objects
  _xpiFile: null,
  _targetDir: null,
  _components: null,
  
  _proxyObject: function (aObject, aIID, aTarget)
  {
    const nsIEventQueueService = Components.interfaces.nsIEventQueueService;
    var eqService = Components.classes["@mozilla.org/event-queue-service;1"]
                              .getService(nsIEventQueueService);
    var uiQ = eqService.getSpecialEventQueue(nsIEventQueueService.UI_THREAD_EVENT_QUEUE);
    
    var proxyObjectManager = Components.classes["@mozilla.org/xpcomproxy;1"]
                                       .getService(Components.interfaces.nsIProxyObjectManager);
    const PROXY_SYNC = 0x01;
    const PROXY_ALWAYS = 0x04;
    this[aTarget] = proxyObjectManager.getProxyForObject(uiQ, aIID, aObject, 
                                                         PROXY_SYNC | PROXY_ALWAYS);
  },
  
  extract: function ()
  {
    const nsIThread = Components.interfaces.nsIThread;
    var thread = Components.classes["@mozilla.org/thread;1"]
                           .createInstance(nsIThread);
    thread.init(this, 0, nsIThread.PRIORITY_NORMAL,
                nsIThread.SCOPE_GLOBAL,
                nsIThread.STATE_JOINABLE);
  },

  /////////////////////////////////////////////////////////////////////////////
  // nsIRunnable
  run: function ()
  { 
    dump("*** RUNNING THREAD\n");
    /*
    var xpiFile = Components.classes["@mozilla.org/file/local;1"]
                            .createInstance(Components.interfaces.nsILocalFile);
    xpiFile.initWithPath(this._xpiFile);
    var targetDir = Components.classes["@mozilla.org/file/local;1"]
                              .createInstance(Components.interfaces.nsILocalFile);
    targetDir.initWithPath(this._targetDir);
    
    var zipReader = Components.classes["@mozilla.org/libjar/zip-reader;1"]
                              .createInstance(Components.interfaces.nsIZipReader);
    zipReader.init(xpiFile);

    var entries = zipReader.findEntries("*");
    while (entries.hasMoreElements()) {
      var entry = entries.getNext().QueryInterface(Components.interfaces.nsIZipEntry);
      dump("*** zip entry = " + entry.name + "\n");
    }
    */
  }
};

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 = {
  /////////////////////////////////////////////////////////////////////////////
  // 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");
    
      var fileLocator = Components.classes["@mozilla.org/file/directory_service;1"]
                                  .getService(Components.interfaces.nsIProperties);
      var autoregFile = fileLocator.get("ProfD", Components.interfaces.nsIFile);
      autoregFile.append(".autoreg");
      if (autoregFile.exists()) {
        var ww = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
                           .getService(Components.interfaces.nsIWindowWatcher);
        var win = ww.openWindow(null, "chrome://mozapps/content/extensions/finalize.xul", 
                                "", "chrome,centerscreen,dialog", null);
      
        // 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._finalizeEnable(items[i]);
        
        // Look for extensions that need to be disabled
        items = this._ds.getItemsWithFlagSet("toBeDisabled");
        for (var i = 0; i < items.length; ++i)
          this._finalizeDisable(items[i]);
        
        // Look for extensions that need to be removed
        items = this._ds.getItemsWithFlagSet("toBeUninstalled");
        dump("*** items.length = " + items.length + "\n");
        for (var i = 0; i < items.length; ++i)
          this._finalizeUninstall(items[i]);
          
        win.close();
        
        // Now that we've finalized the EM operation, remove the autoreg file so
        // we don't do this every time we start.
        autoregFile.remove(false);
      }
      
      // Load default preferences files for all extensions
      
      var defaultsManifest = fileLocator.get("ProfD", Components.interfaces.nsIFile);
      defaultsManifest.append("extensions");
      defaultsManifest.append("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.charAt(path.length-1) == "\n")
            path = path.substr(0, path.length - 1);
          lf.initWithPath(path);
          
          if (lf.exists())
            pref.readUserPrefs(lf);
        }
        while (more);
        fis.close();
      }
      break;
    }
  },
  
  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, Components.interfaces.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;
  },
  
  _writeProfileFile: function (aSubfolder, aFileName, 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 fileLocator = Components.classes["@mozilla.org/file/directory_service;1"]
                                .getService(Components.interfaces.nsIProperties);
    var profileDir = fileLocator.get("ProfD", Components.interfaces.nsIFile);
    var globalDir = fileLocator.get("ProfD", Components.interfaces.nsIFile); // XXXben - XCurProcDir
    
    // Open the .autoreg file for writing.
    
    var dataFile = profileDir.clone();
    if (aSubfolder)
      dataFile.append(aSubfolder);
    dataFile.append(aFileName);
    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(dataFile, MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE, 0664, 0);

    var extensions = this.getItemList(null, Components.interfaces.nsIUpdateItem.TYPE_EXTENSION, { });
    for (var i = 0; i < extensions.length; ++i) {
      var extension = extensions[i];
      
      var valid = this._ds.getExtensionProperty(extension.id, "toBeDisabled") != "true";
      valid = this._ds.getExtensionProperty(extension.id, "toBeUninstalled") != "true";
      
      if (valid) continue;
      
      // Now locate this extension within the extensions folder
      // First, where is it installed - profile or global?
      var location = this._ds.getExtensionProperty(extension.id, "installLocation");
      var dir = location == "profile" ? profileDir : globalDir;
      
      // Now synthesize the components dir for the extension
      var sourceDir = aGetDirFunc(dir, extension.id);
      if (sourceDir.exists()) {
        // write the relative path of the components dir
        aWriteLineFunc(fos, sourceDir, extension.id);
      }
    }
    fos.close();
  },
  
  _getComponentsDir: function (aSourceDir, aExtensionID)
  {
    var sourceDir = aSourceDir.clone();
    sourceDir.append("extensions");
    sourceDir.append(aExtensionID);
    sourceDir.append("components");
    return sourceDir;
  },
  
  _getPreferencesDir: function (aSourceDir, aExtensionID)
  {
    var sourceDir = aSourceDir.clone();
    sourceDir.append("extensions");
    sourceDir.append(aExtensionID);
    sourceDir.append("defaults");
    sourceDir.append("preferences");
    return sourceDir;
  },
  
  _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);
    }
  },

  _writeProfileData: function ()
  {
    this._writeProfileFile(null, ".autoreg", 
                           this._getComponentsDir, 
                           this._writeAutoregLines);
    this._writeProfileFile("extensions", "Defaults", 
                           this._getPreferencesDir, 
                           this._writePreferencesLines);
  },
  
  /////////////////////////////////////////////////////////////////////////////  
  // nsIExtensionManager
  installExtensionFromXPI: function (aZipReader, 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 tempManifest = getDir(installProfile ? "ProfD" : "ProfD", 
                              ["extensions", "temp"]);
    tempManifest.append("extension.rdf");
    aZipReader.extract("extension.rdf", 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...
    var extensionID = this._canInstallExtension(ds);
    if (extensionID) {
      this._ensureDS();
      this._ds.addPendingExtensionEntry(extensionID, installProfile);
      // Then we stage the extension's XPI into a temporary directory so we 
      // can extract them after the next restart. 
      this._stageExtensionXPI(aZipReader, extensionID, installProfile);
    }
    tempManifest.remove(false);
    
    this._writeProfileData();
  },
  
  _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");
    var id = rdf.GetResource(EM_NS("id"));
    // XXXben - do version check
    var idLiteral = aDataSource.GetTarget(manifestRoot, id, true);
    return idLiteral.QueryInterface(Components.interfaces.nsIRDFLiteral).Value;
  },
  
  _stageExtensionXPI: function (aZipReader, aExtensionID, aInstallProfile)
  {
    // Get the staging dir
    var dir = getDir(aInstallProfile ? "ProfD" : "ProfD", 
                     ["extensions", "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)
  {
    var installLocation = this._ds.getExtensionProperty(aExtensionID, "installLocation");
    var isProfile = installLocation == "profile";
    var logger = new nsInstallLogger(aExtensionID, isProfile);
    
    // 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(aExtensionID, logger);
    
    // Load the metadata datasource
    var metadataFile = getDir(isProfile ? "ProfD" : "ProfD", 
                              ["extensions", aExtensionID]); // XXXben XCurProcDir
    metadataFile.append("extension.rdf");
    
    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 metadataDS = rdf.GetDataSourceBlocking(fph.getURLSpecFromFile(metadataFile));
    
    // Add metadata for the extension to the global extension metadata set
    this._ds.addExtensionMetaData(aExtensionID, metadataDS, isProfile);
    
    // Register chrome packages for files specified in the extension manifest
    this._registerChromeForExtension(aExtensionID, metadataDS, logger);
    
    // Unset the "toBeInstalled" flag
    this._ds.removePendingExtensionEntry(aExtensionID, isProfile);
  },
  
  _installExtensionFiles: function (aExtensionID, aLogger, aIsProfile)
  {
    var sourceXPI = getDir(aIsProfile ? "ProfD" : "ProfD", 
                           ["extensions", "temp", aExtensionID]); // XXXben XCurProcDir
    sourceXPI.append(aExtensionID + ".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 = ["extensions", aExtensionID];
      for (var i = 0; i < parts.length - 1; ++i)
        subDirs.push(parts[i]);
      
      var targetFile = getDir(aIsProfile ? "ProfD" : "ProfD", subDirs);
      var fileName = parts[parts.length-1];
      if (fileName != "") {
        targetFile.append(fileName);
        zipReader.extract(entry.name, targetFile);
        aLogger.addFile(targetFile);
      }
    }
    zipReader.close();
    // Kick off the extraction on a new thread, then join to wait for it to
    // complete. 
    // (new nsJarFileExtractor(aZipReader.file, dir)).extract();
  },
  
  _registerChromeForExtension: function (aExtensionID, aLogger)
  {
    // Enumerate the metadata datasource files collection and register chrome
    // for each file
  },
  
  _unregisterChromeForExtension: function (aExtensionID, aLogger)
  {
  
  },
  
  _finalizeEnable: function (aExtensionID)
  {
  
  },
  
  _finalizeDisable: function (aExtensionID)
  {
  
  },
  
  _finalizeUninstall: function (aExtensionID)
  {
    dump("*** trying to finalize uninstall for " + aExtensionID + "\n");
  },
      
  uninstallExtension: function (aExtensionID)
  {
    this._ds.uninstallExtension(aExtensionID);
    this._writeProfileData();
  },
  
  enableExtension: function (aExtensionID)
  {
    this._ds.enableExtension(aExtensionID);
    this._writeProfileData();
  },
  
  disableExtension: function (aExtensionID)
  {
    this._ds.disableExtension(aExtensionID);
    this._writeProfileData();
  },
  
  // XXXben - handle the case where the item provides its own update url. 
  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 = Components.interfaces.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) {
      this._ds = new nsExtensionsDataSource();
      if (this._ds) {
        this._ds.loadExtensions(false);
        this._ds.loadExtensions(true);
      }
    }
  },

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

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);
  },
  
  /////////////////////////////////////////////////////////////////////////////
  // nsExtensionItemUpdater
  _proxy: null,
  
  _checkForUpdates: function ()
  {
    for (var i = 0; i < this._items.length; ++i) {
      var e = this._items[i];
      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(aResult, "Update:Extension:Item-Error", aMessage);
  },
  
  getNewestExtensionCallback: function (aResult)
  {
    var item = aResult;
    try {
      item.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.
      this._os.notifyObservers(aResult, "Update:Extension:Item-Ended", "goat");
      ++this._updateCount;
    }
    catch (e) {
    }
    
    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 EM_NS(aProperty)
{
  return "http://www.mozilla.org/2004/em-rdf#" + aProperty;
}

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

nsExtensionsDataSource.prototype = {
  _rdf: null,
  
  _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);
  },

  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);
      var checker = new VersionChecker(e, aAppID, aAppVersion, this);
      if (!checker.isCompatible) {
        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, "", "", 
                  Components.interfaces.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, "", "", 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, "", "", 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;
  },
  
  addPendingExtensionEntry: function (aExtensionID, aInstallProfile)
  {
    this._setExtensionProperty(aExtensionID, this._emR("toBeInstalled"), 
                               this._emL("true"), aInstallProfile);
  },
  
  removePendingExtensionEntry: function (aExtensionID, aInstallProfile)
  {
    this._setExtensionProperty(aExtensionID, this._emR("toBeInstalled"), 
                               this._emL("false"), aInstallProfile);
  },
  
  _setProperty: function (aDS, aSource, aProperty, aNewValue)
  {
    var oldValue = aDS.GetTarget(aSource, aProperty, true);
    if (oldValue)
      aDS.Change(aSource, aProperty, oldValue, aNewValue);
    else
      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) {
      var installLocation = this.GetTarget(extension, this._emR("installLocation"), true);
      aIsProfile = installLocation.EqualsNode(this._emL("profile"));
    }
    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"), this._emL("false"));
    this._setExtensionProperty(aExtensionID, this._emR("disabled"), this._emL("false"));
  },
  
  disableExtension: function (aExtensionID)
  {
    this._setExtensionProperty(aExtensionID, this._emR("toBeDisabled"), this._emL("true"));
    this._setExtensionProperty(aExtensionID, this._emR("toBeEnabled"), this._emL("false"));
    this._setExtensionProperty(aExtensionID, this._emR("disabled"), this._emL("true"));
  },
  
  uninstallExtension: function (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);
    
    this._setExtensionProperty(aExtensionID, this._emR("toBeUninstalled"), this._emL("true"));
  },
  
  loadExtensions: function (aProfile)
  {
    var fileLocator = Components.classes["@mozilla.org/file/directory_service;1"].getService(Components.interfaces.nsIProperties);
    var key = aProfile ? "ProfD" : "ProfD"; // XXXben - XCurProcDir
    var extensionsFile = fileLocator.get(key, Components.interfaces.nsIFile);
    extensionsFile.append("extensions");
    
    var extensionsDir = extensionsFile.clone();
    if (!extensionsDir.exists())
      extensionsDir.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0755);
    
    extensionsFile.append("extensions.rdf");
    
    if (!extensionsFile.exists()) {
      // XXXben - Copy over the default file - temporary. 
      if (key == "ProfD") {
        var defaultExtensionsFile = fileLocator.get("ProfDefNoLoc", Components.interfaces.nsIFile);
        defaultExtensionsFile.append("extensions");
        defaultExtensionsFile.append("extensions.rdf");
        defaultExtensionsFile.copyTo(extensionsDir, "extensions.rdf");
      }
      else
        return;
    }

    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

  _appExtensions: null,
  _profileExtensions: null,  
  _composite: null,
  
  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 hasNameArc = this._profileExtensions.hasArcOut(aSource, this._emR("name"));
      var hasVersionArc = this._profileExtensions.hasArcOut(aSource, this._emR("version"));
      return hasNameArc && hasVersionArc ? this._emL("profile") : this._emL("global");
    }
    
    return this._composite.GetTarget(aSource, aProperty, aTruthValue);
  },
  
  GetTargets: function (aSource, aProperty, aTruthValue)
  {
    return this._composite.GetTargets(aSource, aProperty, aTruthValue);
  },
  
  Assert: function (aSource, aProperty, aTarget, aTruthValue)
  {
    return Components.results.NS_RDF_ASSERTION_REJECTED;
  },
  
  Unassert: function (aSource, aProperty, aTarget)
  {
    return Components.results.NS_RDF_ASSERTION_REJECTED;
  },
  
  Change: function (aSource, aProperty, aOldTarget, aNewTarget)
  {
    return Components.results.NS_RDF_ASSERTION_REJECTED;
  },

  Move: function (aSource, aNewSource, aProperty, aTarget)
  {
    return Components.results.NS_RDF_ASSERTION_REJECTED;
  },
  
  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: Components.ID("{8A115FAA-7DCB-4e8f-979B-5F53472F51CF}"),
               contractID: "@mozilla.org/extensions/manager;1",
               className: "Extension Manager",
               factory: {
                          createInstance: function (aOuter, aIID) 
                          {
                            if (aOuter != null)
                              throw Components.results.NS_ERROR_NO_AGGREGATION;
                            
                            if (!gExtensionManager)
                              gExtensionManager = new nsExtensionManager();
                              
                            return gExtensionManager.QueryInterface(aIID);
                          }
                        }
             }
   },
  
  canUnload: function (aComponentManager) 
  {
    return true;
  }
};

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