0
votes

I was comparing this example on MDN: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIProtocolHandler#Implementation

To this add-on on how to create custom protocol: https://addons.mozilla.org/en-US/firefox/files/browse/141969/file/components/AboutFosdem.js#top

Can someone please detail what exactly it's trying to do. The SMTP stuff threw me off.

I couldn't understand what that example on MDN is doing, its doing it whatever its doing without a chrome.manifst. I know the add-on is creating "fosdem://blah" where blah is whatever I want based on the definitions in WhereToGo but it uses a chrome.manifest.

I'm thinking that the mdn example is doing same thing as addon and I would do something like this to set my custom protocol after copy pasting that mdn code:

function myCustomBlah() {}

myCustomBlah.prototype =
  makeProtocolHandler("mycustomblah",
                      -1,
                      "b14c2b67-8680-4c11-8d63-9403c7d4f757"); //i can generate any id

var components = [myCustomBlah];
const NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
2
@noitidart copying stuff into the wiki without any explanation whatsoever. :p - nmaier
Who's noitidart? :P I think Im figuring this out as I read it more :P but they dont show how to properly uninstall it. - Blagoh
JFYI, I reverted the MDN edit. "Adding a mostly useless example that doesn't explain anything at all and uses interfaces only present in comm-central doesn't really help.". While it is certainly a welcome and good thing to improve the wiki, the edits should actually be improvements and not just add to the confusion. - nmaier
Ah I see I just checked the history of that MDN page and the guy that added that confusing example was noit - Blagoh

2 Answers

4
votes

Alrighty lets give a more sensible example of a custom protocol handler.

I decided to implement a ddg: protocol handler, that once registered can be used to type ddg:some search terms into the address bar (among other things) and it will load the DuckDuckGo search page for "some search terms".

The component

One needs to implement the nsIProtocolHandler interface.

What this example component does is "redirect" to DuckDuckGo (well, not really redirect, but it returns a channel for duckduckgo.com). See comments inline.

var {classes: Cc,
     interfaces: Ci,
     manager: Cm,
     results: Cr,
     Constructor: CC
    } = Components;
Cm.QueryInterface(Ci.nsIComponentRegistrar);

Components.utils.import("resource://gre/modules/Services.jsm");
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");

const SCHEME = "ddg";
const DDG_URI = Services.io.newURI("https://duckduckgo.com/?q=%s", null, null);

const nsIURI = CC("@mozilla.org/network/simple-uri;1", "nsIURI");

function DuckDuckGoProtocolHandler() {}
DuckDuckGoProtocolHandler.prototype = Object.freeze({
  classDescription: "DuckDuckGo Protocol Handler",
  contractID: "@mozilla.org/network/protocol;1?name=" + SCHEME,
  classID: Components.ID('{858ea860-129a-11e4-9191-0800200c9a66}'),
  QueryInterface: XPCOMUtils.generateQI([Ci.nsIProtocolHandler]),

  // nsIProtocolHandler
  scheme: SCHEME,
  defaultPort: -1, // No default port.

  // nsIProtocolHandler
  allowPort: function(port, scheme) {
    // This protocol handler does not support ports.
    return false;
  },

  // nsIProtocolHandler
  // Our protocol handler does not support authentication,
  // but it is OK to be loaded from any web-page, not just privileged pages""
  protocolFlags: Ci.nsIProtocolHandler.URI_NOAUTH |
                 Ci.nsIProtocolHandler.URI_LOADABLE_BY_ANYONE,

  // nsIProtocolHandler
  newURI: function(aSpec, aOriginCharset, aBaseURI) {
    // Nothing special here, actually. We were asked to create a new URI.

    // If there is a base-URI, this means that the browser tries to resolve
    // a dependent resource (an image, script) or the user clicked on a relative link.
    // In this case we cannot really return another "ddg" URI, but need to return
    // the proper https URI.
    if (aBaseURI && aBaseURI.scheme == SCHEME) {
      return Services.io.newURI(aSpec, aOriginCharset, DDG_URI);
    }

    // We don't care about the charset, so just ignore that
    // (we support what nsIURI supports).
    let rv = new nsIURI();
    rv.spec = aSpec;
    return rv;
  },

  // nsIProtocolHandler
  newChannel: function(aURI) {
    // We were asked to open a new channel.
    // We could implement an entirely custom channel that supports
    // (most of) nsIChannel. But that is tremendous work and outside
    // of the scope of this basic example (which is about protocol handlers and
    // not channels).
    // Or we can just return any other channel we can create.
    // Since we're going to implement the "ddg:" protocol, lets just open a
    // regular https channel to duckduckgo.com, use the URI as the search term
    // and return that channel.
    let spec = DDG_URI.spec.replace("%s", aURI.path);
    let channel = Services.io.newChannel(spec, aURI.originCharset, null);

    // Setting .originalURI will not only let other code know where this
    // originally came from, but the UI will actually show that .originalURI.
    channel.originalURI = aURI;

    return channel;
  }
});

Component registration in chrome.manifest

We need to implement NSGetFactory if our component is a JavaScript component registered via chrome.manifest. Luckily, XPCOMUtils.jsm has a helper for that.

var NSGetFactory =
  XPCOMUtils.generateNSGetFactory([DuckDuckGoProtocolHandler]);

Registration in bootstrapped add-ons (and Scratchpad)

In bootstrapped/restartless add-ons (incl. SDK add-ons) and Scratchpad, one will need to register the component manually, as chrome.manifest registration is not available.

One could register the result of NSGetFactory(classID), but here is some code creating a Factory manually and registering it.

function Factory(component) {
  this.createInstance = function(outer, iid) {
    if (outer) {
      throw Cr.NS_ERROR_NO_AGGREGATION;
    }
    return new component();
  };
  this.register = function() {
    Cm.registerFactory(component.prototype.classID,
                       component.prototype.classDescription,
                       component.prototype.contractID,
                       this);
  };
  this.unregister = function() {
    Cm.unregisterFactory(component.prototype.classID, this);
  }
    Object.freeze(this);
  this.register();
}
var factory = new Factory(DuckDuckGoProtocolHandler);

Please note that in restartless add-ons you'll also need to unregister it again on shutdown!

factory.unregister();

Testing in a Scratchpad

Copy the component code and the manual registration code into a scratchpad, set the Enviroment to Browser, and run it. Then open ddg:some search terms in a tab ;)

It works!

1
votes

Aw dang you beat me to it.

I totally understand about that removal from MDN.

I wrote this example, it has one problem, it changes the URL. I'll read your solution and compare to mine.

btw I lifted this from about:addons-memory addon. In this example, if you type in somecustomblah:noitidart. it will take you to twitter.com/noitidart. The problem is, the url changes from somecustomblah:noitidart to twitter.com/Noitidart/, ill try to fix, i think your solution might have the answer @nmaier. This way doesn't require chrome.manifest and also you can register it with reigsterComponents and unregister it with unregisterComponents.

Make sure to generate your own UUID per your protocol: http://www.famkruithof.net/uuid/uuidgen

A third argument of aDefaultPort allows you to do that SMTP thing that moz central was doing. You can omit it, or make it null or undefined if you don't want to do that SMTP thing whree you use ports.

var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr, manager: Cm} = Components; //can make this const if not in scratchpad
Cm.QueryInterface(Ci.nsIComponentRegistrar);
Cu.import('resource://gre/modules/Services.jsm');
Cu.import('resource://gre/modules/XPCOMUtils.jsm');

var nsIProtocolHandler = Ci.nsIProtocolHandler; //const

var unloaders = [];

function makeProtocolHandler(aProtocol, aClassID, aDefaultPort) {
    var obj = {
        classID: Components.ID(aClassID),
        classDescription: 'blah blah blah',
        contractID: '@mozilla.org/network/protocol;1?name=' + aProtocol,
        QueryInterface: XPCOMUtils.generateQI([nsIProtocolHandler]),
        scheme: aProtocol,
        defaultPort: aDefaultPort,
        protocolFlags: nsIProtocolHandler.URI_NORELATIVE | nsIProtocolHandler.URI_NOAUTH | nsIProtocolHandler.URI_LOADABLE_BY_ANYONE, //You must specify either URI_LOADABLE_BY_ANYONE, URI_DANGEROUS_TO_LOAD, URI_IS_UI_RESOURCE, or URI_IS_LOCAL_FILE in order for your protocol to work.
        newURI: function(aSpec, aOriginCharset, aBaseURI) {
            var uri = Cc['@mozilla.org/network/simple-uri;1'].createInstance(Ci.nsIURI);
            uri.spec = aSpec;
            console.log('uri=', uri);
            return uri;
        },

        newChannel: function(aURI) {
            //throw Cr.NS_ERROR_NOT_IMPLEMENTED;
            /* Get twitterName from URL */
            var postProtocolPath = aURI.spec.split(":")[1];
            var uri = Services.io.newURI("http://twitter.com/" + postProtocolPath, null, null);
            var channel = Services.io.newChannelFromURI(uri); //, null).QueryInterface(Ci.nsIHttpChannel); //i dont think i need to QI nsIHttpChannel
            /* Determines whether the URL bar changes to the URL */
            //channel.setRequestHeader("X-Moz-Is-Feed", "1", false);
            channel.originalURI = aURI;
            return channel;
        },
        getURIFlags: function(aURI) 0 // i dont think i need this? do I?
    };

    if (aDefaultPort == undefined || aDefaultPort == null) {
        aDefaultPort = -1;
    } else {
        obj.allowPort = function(port, scheme) {
            return port == aDefaultPort;
        };
    }

    return obj;
}

var myComponents = [myCustomBlah];

function myCustomBlah() {}
myCustomBlah.prototype = makeProtocolHandler('mycustomblah', 'b14c2b67-8680-4c11-8d63-9403c7d4f757'); //this uuid (2nd argument) should be generated by you from here: http://www.famkruithof.net/uuid/uuidgen

//const NSGetFactory = XPCOMUtils.generateNSGetFactory(components);


function registerComponents() {
    for (let [y, cls] in Iterator(myComponents)) {
        console.info('y: ', y, 'cls: ', cls);
        try {
            var factory = {
                _cls: cls,
                createInstance: function(outer, iid) {
                    if (outer) {
                        throw Cr.NS_ERROR_NO_AGGREGATION;
                    }
                    return new cls();
                }
            };
            Cm.registerFactory(cls.prototype.classID, cls.prototype.classDescription, cls.prototype.contractID, factory);
            unloaders.push(function() {
                Cm.unregisterFactory(factory._cls.prototype.classID, factory);
            });
        } catch (ex) {
            console.warn('failed to register module: ', cls.name, 'exception thrown: ', ex);
        }
    }
}

function unregisterComponents() {
    for (var i = 0; i < unloaders.length; i++) {
        unloaders[i]();
    }
}

registerComponents(); //run this to make it work. once this is run follwoing examples above: typing "about:yabba" will take you to bings homepage
//unregisterComponents(); //do this to remove it //after running this typing about:yabba will take you to problem loading page