爲firefox添加新的protocol

Adding a New Protocol to Mozilla
本文講述如何爲mozilla添加一個新的協議。實現該協議將要使用javascript和XPCOM,也包括了將其添加到現有的mozilla程序中。

概述

Mozilla支持一般的網絡協議如HTTP和FTP。有時對現存的mozilla程序進行修改以使其支持一種新的協議非常有用,如此mozilla就可以更好的與其他應用程序集成。也許有的人想要mozilla在用戶點擊一個鏈接的時候運行一個IM客戶端,或者一個應用程序需要打開mozilla並在第一個頁面加載之前執行一個動作。
要添加一個新的協議,需要實現一個XPCOM組件。由於XPCOM使得編程語言之間可以相互交互,因此,現在mozilla的XPCOM組件可以用C++或者javascript來實現。本文使用的是javascript,因爲它不需要編譯。
本文的例子是構建一個”search:”協議,它會使用用戶的默認搜索引擎來實現一次搜索

XPCOM的shell

圖1(如下)展示了實現一個新的協議的XPCOM的javascript shell的一個基本結構。
圖1:XPCOM shell的基本結構

    // XPCOM constant definitions
     // Protocol definition   
    // ProtocolFactory definition
    // TestModule definition        
    function NSGetModule(){
      return TestModule;
    }

當XPCOM發現我們的組件的時候會調用NSGetModule(),其返回值是TestModule對象。TestModule實現了XPCOM調用的幾個方法,例如註冊新組件和獲取ProtocolFactory對象,ProtocolFactory運行mozilla在需要的時候創建一個Protocol對象。Protocol對象最終實現這個協議以及幾個XPCOM調用的方法。它包含了在mozilla需要解析該協議的時候運行的代碼,該方法叫做newChannel。
XPCOM shell的代碼快速的過一下,因爲在創建新的協議的時候它們都不需要修改。
XPCOM常量包括我們需要使用的一般的XPCOM組件,以及一些用於新組件的常量。kPROTOCOL_CID是該協議的一個唯一id號,每個新創建的協議都應該有一個使用uuid嘗試的唯一id。kSCHEME定義了該協議的調用方式——本例中是”x-search”,因此要從一個網站調用該協議需要使用"x-search:search term"。
圖2:使用的常量
    const kSCHEME = "x-search";
    const kPROTOCOL_NAME = "Search Protocol";
    const kPROTOCOL_CONTRACTID = "@mozilla.org/network/protocol;1?name=" + kSCHEME;
    const kPROTOCOL_CID = Components.ID("789409b9-2e3b-4682-a5d1-71ca80a76456");

    // Mozilla defined
    const kSIMPLEURI_CONTRACTID = "@mozilla.org/network/simple-uri;1";
    const kIOSERVICE_CONTRACTID = "@mozilla.org/network/io-service;1";
    const nsISupports = Components.interfaces.nsISupports;
    const nsIIOService = Components.interfaces.nsIIOService;
    const nsIProtocolHandler = Components.interfaces.nsIProtocolHandler;
    const nsIURI = Components.interfaces.nsIURI; 

下面是TestModule的代碼。registerSelf使用該組件定義的幾個常量註冊該組件,
圖3:TestModule的代碼
    /**
     * JS XPCOM component registration goop:
     *
     * We set ourselves up to observe the xpcom-startup category.  This provides
     * us with a starting point.
     */

     var TestModule = new Object();
 
     TestModule.registerSelf = function (compMgr, fileSpec, location, type)
     {
       compMgr = compMgr.QueryInterface(Components.interfaces.nsIComponentRegistrar);
       compMgr.registerFactoryLocation(kPROTOCOL_CID,
                                       kPROTOCOL_NAME,
                                       kPROTOCOL_CONTRACTID,
                                       fileSpec, 
                                       location, 
                                       type);
     }

     TestModule.getClassObject = function (compMgr, cid, iid)
     {
       if (!cid.equals(kPROTOCOL_CID))
         throw Components.results.NS_ERROR_NO_INTERFACE;

       if (!iid.equals(Components.interfaces.nsIFactory))
         throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
    
       return ProtocolFactory;
     }

     TestModule.canUnload = function (compMgr)
     {
       return true;
     }

ProtocolFactory所做的就是在實例化的時候都使用createInstance來做一些與XPCOM相關的錯誤檢驗,如果成功則返回一個Protocol對象。
圖4:ProtocolFactory代碼
    var ProtocolFactory = new Object();

    ProtocolFactory.createInstance = function (outer, iid)
    {
      if (outer != null)
        throw Components.results.NS_ERROR_NO_AGGREGATION;

      if (!iid.equals(nsIProtocolHandler) && !iid.equals(nsISupports))
        throw Components.results.NS_ERROR_NO_INTERFACE;
    
      return new Protocol();
    }

最後,Protocol對象實現該協議的功能。newChannel()是在使用該協議的時候代碼運行在地點的方法。協議就是通道,爲了讓協議正常工作,其代碼需要實現一個通道。
圖5:Protocol的代碼
    function Protocol()
    {
    }

    Protocol.prototype =
    {
      QueryInterface: function(iid)
      {
        if (!iid.equals(nsIProtocolHandler) &&
            !iid.equals(nsISupports))
          throw Components.results.NS_ERROR_NO_INTERFACE;
        return this;
      },

      scheme: kSCHEME,
      defaultPort: -1,
      protocolFlags: nsIProtocolHandler.URI_NORELATIVE |
                 nsIProtocolHandler.URI_NOAUTH,
  
      allowPort: function(port, scheme)
      {
        return false;
      },

      newURI: function(spec, charset, baseURI)
      {
        var uri = Components.classes[kSIMPLEURI_CONTRACTID].createInstance(nsIURI);
        uri.spec = spec;
        return uri;
      },

      newChannel: function(input_uri)
      {
        // here goes the code that should be run when the protocol gets used.
      }    
    },
  }

協議的實現

最後一步是實現使用該協議的時候所調用的代碼。newChannel調用的時候需要帶有一個參數,是XPCOM的nsIUri類型。要獲取一個字符串版本可以使用其spec成員。該字符串包含了調用該協議所使用的完整URI,這裏是"x-search:[search terms]"。首先要做的是解析掉"x-search:"部分。dump() 是用來向控制檯打印信息的,在調試的時候很有用。
圖6:參數處理

    newChannel: function(aURI)
    {
      // aURI is a nsIUri, so get a string from it using .spec
      var mySearchTerm = aURI.spec;

      // strip away the kSCHEME: part
      mySearchTerm = mySearchTerm.substring(mySearchTerm.indexOf(":") + 1, mySearchTerm.length);    
      mySearchTerm = encodeURI(mySearchTerm);

      dump("[mySearchTerm=" + mySearchTerm + "]\n");
      
      ...
    },

下一步就是獲取用戶的搜索引擎。下面的代碼來自mozilla的navigator.js文件。首先,默認的可靠搜索引擎的首選項是需要的。然後使用一個指向網絡搜索服務的數據RDF數據源來查詢用戶選擇的搜索引擎。
圖7:檢索搜索引擎
    newChannel: function(input_uri)
    {
      ...
      
      var finalURL = "";
    
      try{ 
        // Get the preferences service
        var prefService = Components.classes["@mozilla.org/preferences-service;1"]
                                    .getService(Components.interfaces.nsIPrefService);

        var prefBranch = prefService.getBranch(null);
  
        defaultSearchURL = prefBranch.getComplexValue("browser.search.defaulturl",
                              Components.interfaces.nsIPrefLocalizedString).data;

        var searchDS = Components.classes["@mozilla.org/rdf/datasource;1?name=internetsearch"]
                                 .getService(Components.interfaces.nsIInternetSearchService);

        var searchEngineURI = prefBranch.getCharPref("browser.search.defaultengine");
        if (searchEngineURI) {          
          searchURL = searchDS.GetInternetSearchURL(searchEngineURI, mySearchTerm, 0, 0, {value:0});
          if (searchURL)
            defaultSearchURL = searchURL;      
        }
        dump("[Search Protocol Success: " + defaultSearchURL + "]")
      } catch (e){
        dump("[Search Protocol failed to get the search pref: " + e + "]\n");
      }

      finalURL = defaultSearchURL + mySearchTerm;

      ...
    },

正如之前所描述的那樣,協議就是通道,所以newChannel必須返回一個通道。由於這個協議的目的是運行一次搜索,返回的通道有一些javascript來改變瀏覽器窗口的位置到搜索頁面。這可以通過獲取IOService服務,創建一個新的通道並從newChannel返回這個通道來實現。
圖8:創建一個通道
    newChannel: function(input_uri)
    {
      ...

      /* create dummy nsIChannel instance */
      var ios = Components.classes[kIOSERVICE_CONTRACTID]
                          .getService(nsIIOService);

      return ios.newChannel("javascript:document.location.href='" + finalURL + "'", null, null);

    },

最終newChannel的代碼如下圖9所示。完整的源代碼可以在這裏找到。
圖9:最終Protocol代碼
    newChannel: function(input_uri)
    {
      // aURI is a nsIUri, so get a string from it using .spec
      var mySearchTerm = aURI.spec;

      // strip away the kSCHEME: part
      mySearchTerm = mySearchTerm.substring(mySearchTerm.indexOf(":") + 1, mySearchTerm.length);    
      mySearchTerm = encodeURI(mySearchTerm);

      dump("[mySearchTerm=" + mySearchTerm + "]\n");
      var finalURL = "";
    
      try{ 
        // Get the preferences service
        var prefService = Components.classes["@mozilla.org/preferences-service;1"]
                                    .getService(Components.interfaces.nsIPrefService);

        var prefBranch = prefService.getBranch(null);
  
        defaultSearchURL = prefBranch.getComplexValue("browser.search.defaulturl",
                              Components.interfaces.nsIPrefLocalizedString).data;

        var searchDS = Components.classes["@mozilla.org/rdf/datasource;1?name=internetsearch"]
                                 .getService(Components.interfaces.nsIInternetSearchService);

        var searchEngineURI = prefBranch.getCharPref("browser.search.defaultengine");
        if (searchEngineURI) {          
          searchURL = searchDS.GetInternetSearchURL(searchEngineURI, mySearchTerm, 0, 0, {value:0});
          if (searchURL)
            defaultSearchURL = searchURL;      
        }
        dump("[Search Protocol Success: " + defaultSearchURL + "]")
      } catch (e){
        dump("[Search Protocol failed to get the search pref: " + e + "]\n");
      }

      finalURL = defaultSearchURL + mySearchTerm;

      /* create dummy nsIURI and nsIChannel instances */
      var ios = Components.classes[kIOSERVICE_CONTRACTID]
                          .getService(nsIIOService);

      return ios.newChannel("javascript:document.location='" + finalURL + "'", null, null);

    },

安裝協議

XPCOM組件在mozilla的mozillaDir/components目錄下。要安裝該搜索協議,複製完整的javascript文件到那個目錄。還需要告知Mozilla註冊這個組件(註冊了的組件列在components/目錄下的compreg.dat文件中),可以通過將一個叫做.autoreg的空文件放到mozilla的安裝目錄(與mozilla可執行程序同一級別)下來完成。
在協議安裝完成之後,必須重新啓動mozilla來完成新組件的註冊。然後就可以通過輸入x-search:mozilla到url欄並按enter鍵來調用該協議。
譯註:
上述實例在gecko2.0之前的版本纔可用,經測試,將TestPprotocol.js文件放到firefox的components目錄下,然後在firefox的安裝目錄添加.autoreg文件之後重啓瀏覽器即可在components目錄下的compreg.dat文件中發現TestPprotocol.js已經成功註冊,在地址欄中輸入x-search:mozilla沒什麼反應(在the talk page for nsIProtocolHandler 提到到了上述例子代碼的一些不足,但沒有給出解決方案),但是在註冊該協議之前如果輸入這個並按回車會提示x-search協議未與任何應用程序關聯。

發佈了32 篇原創文章 · 獲贊 83 · 訪問量 29萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章