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协议未与任何应用程序关联。