/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */

/**
 * Implements an application startup observer.
 */

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

function UnMHTStartupObserver() {
}
UnMHTStartupObserver.prototype = {
  // General XPCOM component attributes
  /**
   * インターフェースの要求
   *   nsISupports.QueryInterface
   *
   * @param  nsIIDRef iid
   *         インターフェース ID
   * @throws Components.results.NS_NOINTERFACE
   * @return nsIObserver
   *         this
   */
  QueryInterface : function (iid) {
    if (iid.equals (Ci.nsISupports)
        || iid.equals (Ci.nsIObserver)) {
      return this;
    }
        
    throw Cr.NS_ERROR_NO_INTERFACE;
  },

  // --- nsIObserver interface functions ---

  /**
   * This function is called with aTopic set to "app-startup" when the
   * application starts, before the first browser window is opened.
   */
  observe : function (aSubject, aTopic, aData) {
    UnMHTStartupEvents.onAppStartup ();
  }
};

/**
 * This object handles extension startup and shutdown, and acts as bookkeeper
 * for the related observer registrations. Actual work is delegated to the
 * StartupInitializer object.
 */
var UnMHTStartupEvents = {
  /**
   * This function must be called once on application startup, and simply
   * registers with the host application for other notifications, to be
   * handled by the "observe" function.
   */
  onAppStartup : function () {
    this._observerService.addObserver (this, "profile-after-change", false);
    this._observerService.addObserver (this, "xpcom-shutdown", false);
  },

  /** Called when it is time to unregister all the observers. */
  onAppShutdown : function () {
    this._observerService.removeObserver (this, "profile-after-change");
    this._observerService.removeObserver (this, "xpcom-shutdown");
  },

  /**
   * This function is called every time a new user profile is ready for use in
   * the host application, usually before the first window is opened.
   *
   * Dynamic MIME type and document loader factory registrations must be done
   * here, instead of when the first browser windows loads, to handle the case
   * where an URI managed by this extension is specified on the command-line.
   *
   * This function is hard-coded to enable handling of the existing MHTML mime
   * types, in addition to the types associated with the file extensions
   * ".mht" and ".mhtml".
   *
   * All the initializations done here are temporary (not persisted) and
   * survive until the application is closed. No explicit cleanup is done
   * when a user profile is unloaded.
   */
  afterProfileChange : function () {
    // Register our Document Loader Factory for every handled file type.
    // For all these types, we are the extension that will preferably
    // handle the type.
    var registerer
    = new DlfRegisterer ("@unmht.org/unmht-document-loader-factory;1");
    
    registerer
    = registerer
    .addMimeType      (         "message/rfc822",    true)
    .addMimeType      (         "application/x-mht", true)
    .addMimeType      (         "application/mime",  true)
    .addMimeType      (         "text/x-mht",        true)
    .addFileExtension ("mhtml", "application/x-mht", true)
    .addFileExtension ("mht",   "application/x-mht", true)
    .addFileExtension ("eml",   "message/rfc822",    true);
        
    try {
      var mime = Cc ["@mozilla.org/mime;1"]
        .getService (Ci.nsIMIMEService);
      var systemDefined = mime.getTypeFromExtension ("mht");
            
      registerer.addMimeType (systemDefined, true);
    }
    catch (e) {
    }
        
    registerer.register ();
  },
    
  // --- nsIObserver interface functions ---

  /**
   * This function handles all the notifications related to application
   * startup and shutdown. The aTopic argument is used to distinguish between
   * notifications.
   */
  observe : function (aSubject, aTopic, aData) {
    if (aTopic == "profile-after-change") {
      this.afterProfileChange ();
    }
    if (aTopic == "xpcom-shutdown") {
      this.onAppShutdown ();
    }
  },

  // --- Private methods and properties ---

  _observerService : Cc ["@mozilla.org/observer-service;1"]
  .getService (Ci.nsIObserverService)
};

/**
 * This object takes care of registering the specified Document Loader Factory
 * in the "Gecko-Content-Viewers" category, for specific MIME types or file
 * extensions.
 *
 * To use this object, firstly call the addMimeType or addFileExtension methods,
 * then call the register method.
 *
 * All the changes made by this object are valid only for the current
 * application session.
 *
 * For more information on the document loading process, see
 *  <http://www.mozilla.org/newlayout/doc/webwidget.html> and
 *  <https://developer.mozilla.org/En/The_life_of_an_HTML_HTTP_request>
 *  (retrieved 2008-12-27).
 */
function DlfRegisterer (aDlfContractId) {
  this.dlfContractId = aDlfContractId;
}

DlfRegisterer.prototype = {
  // --- Public properties ---

  /** Contract ID of the Document Loader Factory to be registered */
  dlfContractId : null,

  // --- Public methods and properties ---

  /**
   * Prepare for registering the DLF with the specified MIME type.
   *
   * @param aMimeType           The MIME type to be managed.
   * @param aIsRecommended      True if this DLF is the one that should
   *                            preferably handle the associated type.
   *
   * @return Reference to this object, for fluent interface support.
   */
  addMimeType : function (aMimeType, aIsRecommended) {
    // If the the type was already added with aIsRecommended set to true,
    // leave it alone even if aIsRecommended is now false.
    if (!this._mimeList [aMimeType]) {
      this._mimeList [aMimeType] = aIsRecommended;
    }
    return this;
  },

  /**
   * Prepare for registering the DLF with the MIME type actually associated
   * with the specified file extension.
   *
   * @param aExtension          The file extension to be managed.
   * @param aPossibleMimeType   Suggestion for a possible MIME type to be
   *                            associated with the file extension.
   * @param aIsRecommended      True if this DLF is the one that should
   *                            preferably handle the associated type.
   *
   * @return Reference to this object, for fluent interface support.
   */
  addFileExtension : function (aExtension, aPossibleMimeType,
                               aIsRecommended) {
    // First of all, for the specified file extensions, we must ensure that
    // the application is able to determine a specific MIME type.
    // If there are no other means to determine the MIME type,
    // the last resort is the "ext-to-type-mapping" category,
    // so we set an entry there, replacing an existing entry only for
    // the file extensions that are preferably managed by this DLF.
    this._addCategoryEntryForSession ("ext-to-type-mapping", aExtension,
                                      aPossibleMimeType, aIsRecommended);
 
    // Find out the actual MIME type that will be used for the file
    // extension, and register the DLF for handling it. See also
    // <https://developer.mozilla.org/En/How_Mozilla_determines_MIME_Types>
    // (retrieved 2008-11-21).
    var realMimeType
    = this._getTypeFromExtensionSafely (aExtension, aPossibleMimeType);
    this.addMimeType (realMimeType, aIsRecommended);
    return this;
  },

  /**
   * Perform the actual registration.
   */
  register : function () {
    var isSeaMonkey = false;
        
    try {
      var id
        = Cc ["@mozilla.org/xre/app-info;1"]
        .getService (Ci.nsIXULAppInfo).ID;
      if (id == "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}") {
        /* SeaMonkey */
        isSeaMonkey = true;
      }
    }
    catch (e) {
    }
        
    // For every MIME type that was specified
    for (var mimeTypeToHandle in this._mimeList) {
      if (this._mimeList.hasOwnProperty (mimeTypeToHandle)) {
        // Add the appropriate category entry, without persisting it,
        // and replacing an existing entry only for the MIME types
        // that are preferably managed by this DLF.
                
        if (isSeaMonkey && mimeTypeToHandle == "message/rfc822") {
          continue;
        }
                
        this._addCategoryEntryForSession
        ("Gecko-Content-Viewers",
         mimeTypeToHandle, this.dlfContractId,
         this._mimeList [mimeTypeToHandle]);
      }
    }
  },
    
  // --- Private methods and properties ---

  /**
   * This object will be filled in with properties whose name is the MIME type
   * we need to handle, and whose value is true if this is the DLF that
   * should preferably handle that MIME type.
   */
  _mimeList : {},

  /**
   * Calls nsICategoryManager.addCategoryEntry with aPersist set to false, but
   * if aReplace is false and the category entry already has a value, no
   * exception is thrown.
   * 
   * @param aCategory   The name of the category being modified.
   * @param aEntry      The name of the category entry being modified.
   * @param aValue      The value for the category entry.
   * @param aReplace    A flag indicating whether or not to overwrite the
   *                    value of an existing category entry.
   */
  _addCategoryEntryForSession : function (aCategory, aEntry, aValue,
                                          aReplace) {
    try {
      this._categoryManager.addCategoryEntry
      (aCategory, aEntry, aValue, false, aReplace);
    }
    catch (e) {
      // Ignore only the NS_ERROR_INVALID_ARG result
      if (!(e instanceof Ci.nsIXPCException)
          || (e.result != Cr.NS_ERROR_INVALID_ARG)) {
        throw e;
      }
    }
  },

  /**
   * Calls nsIMIMEService.getTypeFromExtension, and if the call fails
   * unexpectedly, returns the specified MIME type as a fallback.
   */
  _getTypeFromExtensionSafely : function (aExtension, aFallbackMimeType) {
    try {
      return this._mimeService.getTypeFromExtension (aExtension);
    }
    catch (e if (e instanceof Ci.nsIException &&
                 e.result == Cr.NS_ERROR_NOT_INITIALIZED)) {
      // The getTypeFromExtension call may throw NS_ERROR_NOT_INITIALIZED
      //  because of Mozilla bug 484579. In this case, return an arbitrary
      //  MIME type to mitigate the problem.
      return aFallbackMimeType;
    }
  },

  _categoryManager : Cc ["@mozilla.org/categorymanager;1"]
  .getService (Ci.nsICategoryManager),

  // The nsIMIMEService interface is also exposed by the component whose
  // contract ID is "@mozilla.org/uriloader/external-helper-app-service;1",
  // but "@mozilla.org/mime;1" can be used indifferently.
  _mimeService : Cc ["@mozilla.org/mime;1"].getService (Ci.nsIMIMEService)
};

/**
 * 本体のファクトリー
 *   Inherits From: nsIFactory
 */
var UnMHTStartupObserverFactory = {
  /**
   * 本体を生成する
   *   nsIFactory.createInstance
   *
   * @param  nsISupport outer
   *          統合する対象
   * @param  nsIIDRef iid
   *         生成する対象のインターフェース ID
   * @return UnMHTStartupObserver
   *         本体
   */
  createInstance : function (outer, iid) {
    if (outer != null) {
      /* 統合する対象がある場合はエラー */
      throw Components.results.NS_ERROR_NO_AGGREGATION;
    }
        
    return (new UnMHTStartupObserver ()).QueryInterface (iid);
  }
};

/**
 * XPCOM のモジュール
 *   Inherits From: nsIModule
 */
var UnMHTStartupObserverModule = {
  /* 本体に関する情報 */
  CNAME : "UnMHT Startup Observer JS Component",
  CID : Components.ID ("{ce41e549-8fd6-4e02-85f3-e88ce3d23a96}"),
  CONTRACTID : "@unmht.org/unmht-startup-observer;1",
    
  /**
   * インターフェースの要求
   *   nsISupports.QueryInterface
   *
   * @param  nsIIDRef iid
   *         インターフェースID
   * @throws Components.results.NS_NOINTERFACE
   * @return nsIModule
   *         this
   */
  QueryInterface : function (iid) {
    if (iid.equals (Ci.nsISupports)
        || iid.equals (Ci.nsIModule)) {
      return this;
    }
        
    throw Cr.NS_ERROR_NO_INTERFACE;
  },
    
  /**
   * 登録処理
   *   nsIModule.registerSelf
   *
   * @param  nsIComponentManager compMgr
   * @param  nsIFile fileSpec
   *         モジュールのファイル
   *           reference と他のソースコード中で並びが違う
   * @param  String location
   *         不明
   *           reference と他のソースコード中で並びが違う
   * @param  String type
   *         ローダの種類
   */
  registerSelf : function (compMgr, fileSpec, location, type) {
    try {
      var id
      = Cc ["@mozilla.org/xre/app-info;1"]
      .getService (Ci.nsIXULAppInfo).ID;
      if (id == "{3550f703-e582-4d05-9a08-453d09bdfdc6}") {
        /* Thunderbird */
        return;
      }
    }
    catch (e) {
    }
        
    compMgr = compMgr.QueryInterface (Ci.nsIComponentRegistrar);
    compMgr.registerFactoryLocation (this.CID,
                                     this.CNAME,
                                     this.CONTRACTID,
                                     fileSpec, location, type);
        
    var catman
    = Cc ["@mozilla.org/categorymanager;1"]
    .getService (Ci.nsICategoryManager);
    catman.addCategoryEntry ("app-startup",
                             this.CONTRACTID,
                             this.CONTRACTID, true, true);
  },
    
  /**
   * 登録解除処理
   *   nsIModule.unregisterSelf
   *
   * @param  nsIComponentManager compMgr
   * @param  nsIFile fileSpec
   *         モジュールのファイル
   *           reference と他のソースコード中で並びが違う
   * @param  String location
   *         不明
   *           reference と他のソースコード中で並びが違う
   */
  unregisterSelf : function (compMgr, fileSpec, location) {
    try {
      var id
      = Cc ["@mozilla.org/xre/app-info;1"]
      .getService (Ci.nsIXULAppInfo).ID;
      if (id == "{3550f703-e582-4d05-9a08-453d09bdfdc6}") {
        /* Thunderbird */
            
        return;
      }
    }
    catch (e) {
    }
        
    compMgr = compMgr.QueryInterface (Ci.nsIComponentRegistrar);
    compMgr.unregisterFactoryLocation (this.CID, fileSpec);
        
    var catman
    = Cc ["@mozilla.org/categorymanager;1"]
    .getService (Ci.nsICategoryManager);
    catman.deleteCategoryEntry ("app-startup",
                                this.CONTRACTID, true);
  },
    
  /**
   * ファクトリーオブジェクトを取得する
   *   nsIModule.getClassObject
   *
   * @param  nsIComponentManager compMgr
   * @param  nsCIDRef cid
   *         取得対象のクラス ID
   * @param  nsIIDRef iid
   *         取得対象のインターフェース ID
   * @throws Components.results.NS_ERROR_NOT_IMPLEMENTED
   *         Components.results.NS_ERROR_NO_INTERFACE
   * @return UnMHTStartupObserver
   *         本体のファクトリー
   */
  getClassObject : function (compMgr, cid, iid) {
    if (cid.equals (this.CID)) {
      return UnMHTStartupObserverFactory;
    }
        
    if (!iid.equals (Ci.nsIFactory)) {
      throw Cr.NS_ERROR_NOT_IMPLEMENTED;
    }
        
    throw Cr.NS_ERROR_NO_INTERFACE;
  },
    
  /**
   * 終了できるかどうか
   *   nsIModule.canUnload
   *
   * @param  nsIComponentManager compMgr
   * @return Boolean
   *         終了できるかどうか
   */
  canUnload : function (compMgr) {
    return true;
  }
};

// XPCOM component registration
function NSGetModule(compMgr, fileSpec) {
  return UnMHTStartupObserverModule;
}
