Adding a New Protocol to Mozilla
Doron Rosenberg, IBM Corporation.

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.

Figure 1: Basic structure of the XPCOM Shell
    // 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.

Figure 2: Constants 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.

Figure 3: TestModule code

    /**
     * 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.

Figure 4: ProtocolFactory code

    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.

Figure 5: Protocol code
    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.

Figure 6: Handling the argument
    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.

Figure 7: Retrieve the 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.

Figure 8: Creating a Channel
    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.

Figure 9: Final Protocol code
    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.