/* -*- 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 PREF_EM_APP_ID            = "app.id";
const PREF_EM_APP_VERSION       = "app.version";
const PREF_EM_LAST_APP_VERSION  = "extensions.lastAppVersion";
const PREF_UPDATE_EXTENSIONS    = "update.extensions.enabled";
const PREF_UPDATE_AUTOUPDATE    = "update.extensions.autoUpdate";
const PREF_UPDATE_APP           = "update.app.enabled";

function ExtensionItem ()
{
}

ExtensionItem.prototype = {
  init: function (aID, aVersion, aName, aRow, aXPIURL, aType)
  {
    this._id = aID;
    this._version = aVersion;
    this._name = aName;
    this._row = aRow;
    this._xpiURL = aXPIURL;
    this._type = aType;
  },
  
  get id()      { return this._id;      },
  get version() { return this._version; },
  get name()    { return this._name;    },
  get row()     { return this._row;     },
  get xpiURL()  { return this._xpiURL;  },
  get type()    { return this._type;    },

  toObject: function ()
  {
    return { id: this._id, version: this._version, name: this._name, 
             row: this._row, xpiURL: this._xpiURL, type: this._type };
  },

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

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 nsExtensionManager()
{
  var os = Components.classes["@mozilla.org/observer-service;1"]
                     .getService(Components.interfaces.nsIObserverService);
  os.addObserver(this, "profile-after-change", false);
}

nsExtensionManager.prototype = {

  // nsIObserver
  observe: function (aSubject, aTopic, aData)
  {
    if (aTopic == "profile-after-change") {
      var os = Components.classes["@mozilla.org/observer-service;1"]
                        .getService(Components.interfaces.nsIObserverService);
      os.removeObserver(this, "profile-after-change");
      
      // 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);
          }
          this.update(items, items.length, 
                      Components.interfaces.nsIExtensionManager.UPDATE_TYPE_MISMATCH);
        }
      }
      
      // Now update the last app version so we don't do this checking 
      // again.
      pref.setCharPref(PREF_EM_LAST_APP_VERSION, currAppVersion);
    }
  },
  
  // nsIExtensionManager
  installExtensionFromStream: function (aStream, aUseProfile)
  {
    var parser = Components.classes["@mozilla.org/rdf/xml-parser;1"]
                           .createInstance(Components.interfaces.nsIRDFXMLParser);
    var ds = Components.classes["@mozilla.org/rdf/datasource;1?name=in-memory-datasource"]
                       .createInstance(Components.interfaces.nsIRDFDataSource);
    var streamListener = parser.parseAsync(ds, null);
    
    var bytesAvailable;
    do {
      bytesAvailable = aStream.available();
      if (!bytesAvailable)
        break;
      
      streamListener.onDataAvailable(null, null, aStream, 0, bytesAvailable);
    }
    while (1);
    
    this._ensureDS();
    
    this._ds.installExtension(ds, aUseProfile);
  },
      
  uninstallExtension: function (aExtensionID)
  {
    this._ds.uninstallExtension(aExtensionID);
  },
  
  enableExtension: function (aExtensionID)
  {
    this._ds.enableExtension(aExtensionID);
  },
  
  disableExtension: function (aExtensionID)
  {
    this._ds.disableExtension(aExtensionID);
  },
  
  update: function (aItems, aItemCount, aUpdateType)
  {
    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);

    var updater = new nsItemUpdater(aItems, appID, appVersion);
    
    switch (aUpdateType) {
    case Components.interfaces.nsIExtensionManager.UPDATE_TYPE_MISMATCH:
    case Components.interfaces.nsIExtensionManager.UPDATE_TYPE_USERINVOKED:
      var ww = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
                        .getService(Components.interfaces.nsIWindowWatcher);
      var ary = Components.classes["@mozilla.org/supports-array;1"]
                          .createInstance(Components.interfaces.nsISupportsArray);
      ary.AppendElement(updater);
      for (var i = 0; i < aItems.length; ++i)
        ary.AppendElement(aItems[i]);
      ww.openWindow(null, "chrome://mozapps/content/update/update.xul", 
                    "", "chrome,modal,centerscreen", ary);
      break;
    case Components.interfaces.nsIExtensionManager.UPDATE_TYPE_BACKGROUND:
      // The Background Updater Service adds itself as a listener for
      // the various kinds of messages that the updater sends out.
      updater.checkForUpdates();
      break;
    }
  },
  
  // Themes
  installTheme: function (aThemeID)
  {
  
  },
  
  uninstallTheme: function (aThemeID)
  {
  
  },

  get datasource()
  {
    this._ensureDS();
    return this._ds;
  },
  
  //
  _ds: null,
    
  // Other
  _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 nsItemUpdater(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);
}

nsItemUpdater.prototype = {
  /////////////////////////////////////////////////////////////////////////////
  // 
  checkForUpdates: function () 
  {
    this._os.notifyObservers(null, "update-started", "");
    var wspFactory = Components.classes["@mozilla.org/xmlextras/proxy/webserviceproxyfactory;1"]
                              .getService(Components.interfaces.nsIWebServiceProxyFactory);
    wspFactory.createProxyAsync("http://localhost:8080/axis/services/VersionCheck?wsdl", 
                                "VersionCheck", "", true, this);
  },

  _proxy: null,
  
  _checkForUpdates: function ()
  {
    for (var i = 0; i < this._items.length; ++i) {
      var e = this._items[i];
      this._os.notifyObservers(null, "update-item-started", e.name + " " + e.version);
      this._proxy.getNewestExtension(e, 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-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-item-ended", "goat");
    }
    catch (e) {
    }
    
    if (--this._count == 0)
      this._os.notifyObservers(null, "update-ended", "");
  }
};


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

  _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/extensions/item;1"]
                             .createInstance(Components.interfaces.nsIExtensionItem);
        item.init(id, this.getExtensionProperty(id, "version"),
                  this.getExtensionProperty(id, "name"),
                  -1, "", Components.interfaces.nsIExtensionItem.TYPE_EXTENSION);
        items.push(item);
      }
    }
    return items;
  },
  
  _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);
    return this.GetTarget(extension, this._emR(aProperty), true).QueryInterface(Components.interfaces.nsIRDFLiteral).Value;
  },
  
  _setExtensionProperty: function (aExtensionID, aPropertyArc, aPropertyValue)
  {
    var extension = this._rdf.GetResource("urn:mozilla:extension:" + aExtensionID);
    var installLocation = this.GetTarget(extension, this._emR("installLocation"), true);
    var isProfile = installLocation.EqualsNode(this._emR("profile"));
    var ds = isProfile ? this._profileExtensions : this._appExtensions;
    
    this._setProperty(ds, extension, aPropertyArc, aPropertyValue);

    this._flush(isProfile);  
  },
  
  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();
    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 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);
                          }
                        }
             },
    item:    { CID: Components.ID("{F3294B1C-89F4-46F8-98A0-44E1EAE92518}"),
               contractID: "@mozilla.org/extensions/item;1",
               className: "Extension Item",
               factory: {
                          createInstance: function (aOuter, aIID) 
                          {
                            if (aOuter != null)
                              throw Components.results.NS_ERROR_NO_AGGREGATION;
                            
                            return new ExtensionItem().QueryInterface(aIID);
                          }
                        } 
             }  
   },
  
  canUnload: function (aComponentManager) 
  {
    return true;
  }
};

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