Data URIs and XBL

XBL lets you define behaviour by specifying a CSS property. Most of the time, these bindings define how controls act. In the case of user styles, they can do things like moving and removing elements (for ad blocking, for example). And just like people like to include images in their styles by way of data URIs, people want to include XBL bindings in data URIs. In pre-3.0 Firefox, to apply a binding you need to specify a binding ID with your URL by way of a fragment (like http://example.com/binding.xml#theid). The problem that data URIs don’t understand fragments, and “#theid” becomes part of the document and makes the document malformed. In Firefox 3.0 and later, you don’t need to specify an ID; the first binding will be used if no ID is provided, sidestepping the whole problem.

To make it work in Firefox 2.0 and earlier, I considered creating a custom protocol to allow it. I decided against it, but I figured the code might be useful to someone. So here it is. This lets users use the xdata protocol, which works exactly like the data protocol but lets XBL work.

/*----------------------------------------------------------------------
 * Based on:
 *
 * nsChromeExtensionHandler
 * By Ed Anuff <ed@anuff.com>
 * http://kb.mozillazine.org/Dev_:_Extending_the_Chrome_Protocol
 */

/*----------------------------------------------------------------------
 * The ChromeExtension Module
 *----------------------------------------------------------------------
 */

// Custom protocol related
const kSCHEME = "xdata";
const kPROTOCOL_CID = Components.ID("{5360065C-D9B5-11DB-8314-0800200C9A66}");
const kPROTOCOL_CONTRACTID = "@mozilla.org/network/protocol;1?name=" + kSCHEME;
const kPROTOCOL_NAME = "Data URI with fragment support";

// Dummy chrome URL used to obtain a valid chrome channel
// This one was chosen at random and should be able to be substituted
// for any other well known chrome URL in the browser installation
const kDUMMY_CHROME_URL = "chrome://mozapps/content/xpinstall/xpinstallConfirm.xul";

// Mozilla defined
const kCHROMEHANDLER_CID_STR = "{61ba33c0-3031-11d3-8cd0-0060b0fc14a3}";
const kCONSOLESERVICE_CONTRACTID = "@mozilla.org/consoleservice;1";
const kIOSERVICE_CID_STR = "{9ac9e770-18bc-11d3-9337-00104ba0fd40}";
const kIOSERVICE_CONTRACTID = "@mozilla.org/network/io-service;1";
const kNS_BINDING_ABORTED = 0x804b0002;
const kSTANDARDURL_CONTRACTID = "@mozilla.org/network/standard-url;1";
const kURLTYPE_STANDARD = 1;
const nsIComponentRegistrar = Components.interfaces.nsIComponentRegistrar;
const nsIFactory = Components.interfaces.nsIFactory;
const nsIIOService = Components.interfaces.nsIIOService;
const nsIProtocolHandler = Components.interfaces.nsIProtocolHandler;
const nsIRequest = Components.interfaces.nsIRequest;
const nsIStandardURL = Components.interfaces.nsIStandardURL;
const nsISupports = Components.interfaces.nsISupports;
const nsIURI = Components.interfaces.nsIURI;

var ChromeExtensionModule = {
  
  /* CID for this class */
  cid: kPROTOCOL_CID,

  /* Contract ID for this class */
  contractId: kPROTOCOL_CONTRACTID,

  registerSelf : function(compMgr, fileSpec, location, type) {
    compMgr = compMgr.QueryInterface(nsIComponentRegistrar);
    compMgr.registerFactoryLocation(
      kPROTOCOL_CID, 
      kPROTOCOL_NAME, 
      kPROTOCOL_CONTRACTID, 
      fileSpec, 
      location,
      type
    );
  },
  
  getClassObject : function(compMgr, cid, iid) {
    if (!cid.equals(kPROTOCOL_CID)) {
      throw Components.results.NS_ERROR_NO_INTERFACE;
    }
    if (!iid.equals(nsIFactory)) {
      throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
    }
    return this.myFactory;
  },
  
  canUnload : function(compMgr) {
    return true;
  },
  
  myFactory : {
    createInstance : function(outer, iid) {
      if (outer != null) {
        throw Components.results.NS_ERROR_NO_AGGREGATION;
      }
                        
      return new ChromeExtensionHandler().QueryInterface(iid);
    }
  }
};

function NSGetModule(compMgr, fileSpec) {
    return ChromeExtensionModule;
}

/*----------------------------------------------------------------------
 * The ChromeExtension Handler
 *----------------------------------------------------------------------
 */

function ChromeExtensionHandler() {
  this._system_principal = null;
}

ChromeExtensionHandler.prototype = {
  scheme: kSCHEME,
  defaultPort : -1,
  protocolFlags : nsIProtocolHandler.URI_NORELATIVE | nsIProtocolHandler.URI_IS_UI_RESOURCE,
  allowPort : function(port, scheme) { return false; },
  
  newURI : function(spec, charset, baseURI) {
    var new_url = Components.classes[kSTANDARDURL_CONTRACTID].createInstance(nsIStandardURL);
    new_url.init(kURLTYPE_STANDARD, -1, spec, charset, baseURI);    
    var new_uri = new_url.QueryInterface(nsIURI);
    return new_uri;
  },
  
  newChannel : function(uri) {
    var chrome_service = Components.classesByID[kCHROMEHANDLER_CID_STR].getService().QueryInterface(nsIProtocolHandler);
    var new_channel = null;
    try {
      var uri_string = uri.spec.toLowerCase();
        
      if (uri_string.indexOf(kSCHEME) == 0) {
        var ioService = Components.classesByID[kIOSERVICE_CID_STR].getService();
        ioService = ioService.QueryInterface(nsIIOService);
        if (this._system_principal == null) {
          var chrome_uri_str = kDUMMY_CHROME_URL;
          var chrome_uri = chrome_service.newURI(chrome_uri_str, null, null);
          var chrome_channel = chrome_service.newChannel(chrome_uri);
          this._system_principal = chrome_channel.owner;

          var chrome_request = chrome_channel.QueryInterface(nsIRequest);
          chrome_request.cancel(kNS_BINDING_ABORTED);
        }
				var uri_str = uri.spec.replace(new RegExp("^" + kSCHEME + ":/*"), "data:").replace(/#.*$/, "");
				var ext_uri = ioService.newURI(uri_str, null, null);
				var ext_channel = ioService.newChannelFromURI(ext_uri);

        if (this._system_principal != null) {
          ext_channel.owner = this._system_principal;
        }

        ext_channel.originalURI = uri;          
        return ext_channel;

      }
      
      if (uri_string.indexOf("chrome") != 0) {
        uri_string = uri.spec;
        uri_string = "chrome" + uri_string.substring(uri_string.indexOf(":"));
        uri = chrome_service.newURI(uri_string, null, null);
      }
      
      new_channel = chrome_service.newChannel(uri);
      
    } catch (e) {
      throw Components.results.NS_ERROR_FAILURE;
    }
    
    return new_channel;
  },
  
  QueryInterface : function(iid) {
    if (!iid.equals(Components.interfaces.nsIProtocolHandler) &&
      !iid.equals(Components.interfaces.nsISupports)) {
      throw Components.results.NS_ERROR_NO_INTERFACE;
    }
    return this;
  }
};

Leave a Reply

Adventures in development - Web standards and Firefox extensions