This article demonstrates how to add a new protocol to Mozilla. JavaScript and XPCOM will be used to implement the protocol, and will also cover adding it to an existing Mozilla installation.
Introduction
Mozilla supports the generic web protocols, such as http and ftp. It is sometimes usefull to modify existing Mozilla installations to support a new protocol so that Mozilla is able to integrate better with other applications. Perhaps one wants Mozilla to launch an Instant Messenger client when a user clicks on a certain link, or even if an application wants to open Mozilla and have it perform an action before the first page is loaded.
To add a new protocol, one needs to implement an XPCOM component. Since XPCOM allows programming languages to talk to eachother, XPCOM components can be implemented in C++ or JavaScript currently in Mozilla. For this article, JavaScript will be used, since it requires no compiling.
The example this article builds will be the "search:" protocol, which will use the user's default search engine to initiate a search.
The XPCOM Shell
Figure 1 (below) shows the basic structure of the XPCOM JavaScript shell needed to
implemenet a new protocol. NSGetModule()
will get called when XPCOM
finds our component, which returns the TestModule object. TestModule implements
several methods XPCOM calls, such as registering the new component and getting an
ProtocolFactory object. ProtocolFactory allows the creation of an Protocol object when
Mozilla requires one. Protocol finally implements the protocal, and has several methods that XPCOM calls. It contains
the code that will be run when Mozilla needs to resolve the protocol, in the method called
newChannel
.
The XPCOM shell code will be covered quickly, as none of it has to be modified to create a new protocol.
// XPCOM constant definitions // Protocol definition // ProtocolFactory definition // TestModule definition function NSGetModule(){ return TestModule; }
The XPCOM constants consit of general XPCOM components we want to use, as well
as some constants for the new component. kPROTOCOL_CID
is a unique
id for this particular protocol, and each new protocol created should have one
generated for it using uuid. kSCHEME
defines the way the protocol
is called - in this case, it is "x-search", therefore to call this protocol from
a website, "x-search:search term" will be used.
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;
Below is the TestModule code. registerSelf
registers the component,
using several constants defined in the component.
/** * 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; }
All ProtocolFactory does when instantiated using createInstance
is
to do some XPCOM-related error checking, which if succeeds returns an Protocol
object.
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(); }
Finally, the Protocol object implements the funtionality of the protocol.
newChannel()
is the method where the code goes that should be run
when the protocol is used. Protocols are channels, and the code will have to
implement one for it to work.
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. } }, }
Implementing the Protocol
The final step is implementing the code that gets called when the protocol
is used. newChannel
is called with one argument, which is of the XPCOM
type nsIUri
. To get a string version, the spec
member is used. The string
will contain the full URI used to call the protocol, in this case "x-search:[search terms]".
The first thing needed is to parse away the "x-search:" part. dump()
prints to the console and is usefull for debugging purposes.
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"); ... },
Next, it is time to get the user's search engine. The following code was taken
from Mozilla's navigator.js
file. First, preferences are queried
for the default search engine as a fallback. After that, a RDF datasource
pointing to the internet search service data is used to query the user's selected
search engine.
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; ... },
As stated before, protocols are channels, and newChannel
has to
return one. Since the objective of the protocol is to run a search, a
channel is returned with some JavaScript to change the location of the browser
window to the search page. This is done by getting the IOService
service, creating a new channel and then returning it from newChannel
.
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); },
The final newChannel
code is listed below in Figure 9. The full
sourcecode for the protocol can be found here.
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); },
Installing the Protocol
XPCOM components in Mozilla live in the mozillaDir/components
directory.
Inorder to install the search protocol, copy the
full JavaScript file there. Mozilla also needs to
be told to reregister the components (components that are registered are listed in
the compreg.dat
file in the components/ directory), which is achieved by placing an
empty file called .autoreg
into Mozilla's install directory, at the same
level where the Mozilla executable lives.
After the protocol is installed, Mozilla has to be restarted so that it can register
the new component. The protocol can then be called by typing x-search:mozilla
into the url bar and pressing enter.