--- a/uriloader/exthandler/nsHandlerService.js
+++ b/uriloader/exthandler/nsHandlerService.js
@@ -65,18 +65,19 @@ const NC_HANDLER_INFO = NC_NS + "h
// nsIHandlerInfo::preferredAction
const NC_SAVE_TO_DISK = NC_NS + "saveToDisk";
const NC_HANDLE_INTERNALLY = NC_NS + "handleInternal";
const NC_USE_SYSTEM_DEFAULT = NC_NS + "useSystemDefault";
// nsIHandlerInfo::alwaysAskBeforeHandling
const NC_ALWAYS_ASK = NC_NS + "alwaysAsk";
-// references nsIHandlerApp record
+// references nsIHandlerApp records
const NC_PREFERRED_APP = NC_NS + "externalApplication";
+const NC_POSSIBLE_APP = NC_NS + "possibleApplication";
// handler app ("handler") properties
// nsIHandlerApp::name
const NC_PRETTY_NAME = NC_NS + "prettyName";
// nsILocalHandlerApp::executable
const NC_PATH = NC_NS + "path";
@@ -115,16 +116,17 @@ HandlerService.prototype = {
// FIXME: when we switch from RDF to something with transactions (like
// SQLite), enclose the following changes in a transaction so they all
// get rolled back if any of them fail and we don't leave the datastore
// in an inconsistent state.
this._ensureRecordsForType(aHandlerInfo);
this._storePreferredAction(aHandlerInfo);
this._storePreferredHandler(aHandlerInfo);
+ this._storePossibleHandlers(aHandlerInfo);
this._storeAlwaysAsk(aHandlerInfo);
// Write the changes to the database immediately so we don't lose them
// if the application crashes.
if (this._ds instanceof Ci.nsIRDFRemoteDataSource)
this._ds.Flush();
},
@@ -159,77 +161,149 @@ HandlerService.prototype = {
// Storage Methods
_storePreferredAction: function HS__storePreferredAction(aHandlerInfo) {
var infoID = this._getInfoID(aHandlerInfo);
switch(aHandlerInfo.preferredAction) {
case Ci.nsIHandlerInfo.saveToDisk:
this._setLiteral(infoID, NC_SAVE_TO_DISK, "true");
- this._removeValue(infoID, NC_HANDLE_INTERNALLY);
- this._removeValue(infoID, NC_USE_SYSTEM_DEFAULT);
+ this._removeTarget(infoID, NC_HANDLE_INTERNALLY);
+ this._removeTarget(infoID, NC_USE_SYSTEM_DEFAULT);
break;
case Ci.nsIHandlerInfo.handleInternally:
this._setLiteral(infoID, NC_HANDLE_INTERNALLY, "true");
- this._removeValue(infoID, NC_SAVE_TO_DISK);
- this._removeValue(infoID, NC_USE_SYSTEM_DEFAULT);
+ this._removeTarget(infoID, NC_SAVE_TO_DISK);
+ this._removeTarget(infoID, NC_USE_SYSTEM_DEFAULT);
break;
case Ci.nsIHandlerInfo.useSystemDefault:
this._setLiteral(infoID, NC_USE_SYSTEM_DEFAULT, "true");
- this._removeValue(infoID, NC_SAVE_TO_DISK);
- this._removeValue(infoID, NC_HANDLE_INTERNALLY);
+ this._removeTarget(infoID, NC_SAVE_TO_DISK);
+ this._removeTarget(infoID, NC_HANDLE_INTERNALLY);
break;
// This value is indicated in the datastore either by the absence of
// the three properties or by setting them all "false". Of these two
// options, the former seems preferable, because it reduces the size
// of the RDF file and thus the amount of stuff we have to parse.
case Ci.nsIHandlerInfo.useHelperApp:
default:
- this._removeValue(infoID, NC_SAVE_TO_DISK);
- this._removeValue(infoID, NC_HANDLE_INTERNALLY);
- this._removeValue(infoID, NC_USE_SYSTEM_DEFAULT);
+ this._removeTarget(infoID, NC_SAVE_TO_DISK);
+ this._removeTarget(infoID, NC_HANDLE_INTERNALLY);
+ this._removeTarget(infoID, NC_USE_SYSTEM_DEFAULT);
break;
}
},
_storePreferredHandler: function HS__storePreferredHandler(aHandlerInfo) {
var infoID = this._getInfoID(aHandlerInfo);
var handlerID = this._getPreferredHandlerID(aHandlerInfo);
var handler = aHandlerInfo.preferredApplicationHandler;
if (handler) {
- // First add a record for the preferred app to the datasource. In the
- // process we also need to remove any vestiges of an existing record, so
- // we remove any properties that we aren't overwriting.
- this._setLiteral(handlerID, NC_PRETTY_NAME, handler.name);
- if (handler instanceof Ci.nsILocalHandlerApp) {
- this._setLiteral(handlerID, NC_PATH, handler.executable.path);
- this._removeValue(handlerID, NC_URI_TEMPLATE);
- }
- else {
- handler.QueryInterface(Ci.nsIWebHandlerApp);
- this._setLiteral(handlerID, NC_URI_TEMPLATE, handler.uriTemplate);
- this._removeValue(handlerID, NC_PATH);
- }
+ this._storeHandlerApp(handlerID, handler);
- // Finally, make this app be the preferred app for the handler info.
- // Note: at least some code completely ignores this setting and assumes
- // the preferred app is the one whose URI follows the appropriate pattern.
+ // Make this app be the preferred app for the handler info.
+ //
+ // Note: nsExternalHelperAppService::FillContentHandlerProperties ignores
+ // this setting and instead identifies the preferred app as the resource
+ // whose URI follows the pattern urn:<class>:externalApplication:<type>.
+ // But the old downloadactions.js code used to set this property, so just
+ // in case there is still some code somewhere that relies on its presence,
+ // we set it here.
this._setResource(infoID, NC_PREFERRED_APP, handlerID);
}
else {
// There isn't a preferred handler. Remove the existing record for it,
// if any.
- this._removeValue(handlerID, NC_PRETTY_NAME);
- this._removeValue(handlerID, NC_PATH);
- this._removeValue(handlerID, NC_URI_TEMPLATE);
- this._removeValue(infoID, NC_PREFERRED_APP);
+ this._removeTarget(infoID, NC_PREFERRED_APP);
+ this._removeAssertions(handlerID);
+ }
+ },
+
+ /**
+ * Store the list of possible handler apps for the content type represented
+ * by the given handler info object.
+ *
+ * @param aHandlerInfo {nsIHandlerInfo} the handler info object
+ */
+ _storePossibleHandlers: function HS__storePossibleHandlers(aHandlerInfo) {
+ var infoID = this._getInfoID(aHandlerInfo);
+
+ // First, retrieve the set of handler apps currently stored for the type,
+ // keeping track of their IDs in a hash that we'll use to determine which
+ // ones are no longer valid and should be removed.
+ var currentHandlerApps = {};
+ var currentHandlerTargets = this._getTargets(infoID, NC_POSSIBLE_APP);
+ while (currentHandlerTargets.hasMoreElements()) {
+ let handlerApp = currentHandlerTargets.getNext();
+ if (handlerApp instanceof Ci.nsIRDFResource) {
+ let handlerAppID = handlerApp.Value;
+ currentHandlerApps[handlerAppID] = true;
+ }
+ }
+
+ // Next, store any new handler apps.
+ var newHandlerApps =
+ aHandlerInfo.possibleApplicationHandlers.enumerate();
+ while (newHandlerApps.hasMoreElements()) {
+ let handlerApp =
+ newHandlerApps.getNext().QueryInterface(Ci.nsIHandlerApp);
+ let handlerAppID = this._getPossibleHandlerAppID(handlerApp);
+ if (!this._hasResourceTarget(infoID, NC_POSSIBLE_APP, handlerAppID)) {
+ this._storeHandlerApp(handlerAppID, handlerApp);
+ this._addResourceTarget(infoID, NC_POSSIBLE_APP, handlerAppID);
+ }
+ delete currentHandlerApps[handlerAppID];
+ }
+
+ // Finally, remove any old handler apps that aren't being used anymore,
+ // and if those handler apps aren't being used by any other type either,
+ // then completely remove their record from the datastore so we don't
+ // leave it clogged up with information about handler apps we don't care
+ // about anymore.
+ for (let handlerAppID in currentHandlerApps) {
+ this._removeTarget(infoID, NC_POSSIBLE_APP, handlerAppID);
+ if (!this._existsResourceTarget(NC_POSSIBLE_APP, handlerAppID))
+ this._removeAssertions(handlerAppID);
+ }
+ },
+
+ /**
+ * Store the given handler app.
+ *
+ * Note: the reason this method takes the ID of the handler app in a param
+ * is that the ID is different than it usually is when the handler app
+ * in question is a preferred handler app, so this method can't just derive
+ * the ID of the handler app by calling _getPossibleHandlerAppID, its callers
+ * have to do that for it.
+ *
+ * @param aHandlerAppID {string} the ID of the handler app to store
+ * @param aHandlerApp {nsIHandlerApp} the handler app to store
+ */
+ _storeHandlerApp: function HS__storeHandlerApp(aHandlerAppID, aHandlerApp) {
+ aHandlerApp.QueryInterface(Ci.nsIHandlerApp);
+ this._setLiteral(aHandlerAppID, NC_PRETTY_NAME, aHandlerApp.name);
+
+ // In the case of the preferred handler, the handler ID could have been
+ // used to refer to a different kind of handler in the past (i.e. either
+ // a local hander or a web handler), so if the new handler is a local
+ // handler, then we remove any web handler properties and vice versa.
+ // This is unnecessary but harmless for possible handlers.
+
+ if (aHandlerApp instanceof Ci.nsILocalHandlerApp) {
+ this._setLiteral(aHandlerAppID, NC_PATH, aHandlerApp.executable.path);
+ this._removeTarget(aHandlerAppID, NC_URI_TEMPLATE);
+ }
+ else {
+ aHandlerApp.QueryInterface(Ci.nsIWebHandlerApp);
+ this._setLiteral(aHandlerAppID, NC_URI_TEMPLATE, aHandlerApp.uriTemplate);
+ this._removeTarget(aHandlerAppID, NC_PATH);
}
},
_storeAlwaysAsk: function HS__storeAlwaysAsk(aHandlerInfo) {
var infoID = this._getInfoID(aHandlerInfo);
this._setLiteral(infoID,
NC_ALWAYS_ASK,
aHandlerInfo.alwaysAskBeforeHandling ? "true" : "false");
@@ -372,16 +446,40 @@ HandlerService.prototype = {
* @returns {string} the ID
*/
_getPreferredHandlerID: function HS__getPreferredHandlerID(aHandlerInfo) {
return "urn:" + this._getClass(aHandlerInfo) + ":externalApplication:" +
aHandlerInfo.type;
},
/**
+ * Return the unique identifier for a handler app record, which stores
+ * information about a possible handler for one or more content types,
+ * including its human-readable name and the path to its executable (for a
+ * local app) or its URI template (for a web app).
+ *
+ * Note: handler app IDs for preferred handlers are different. For those,
+ * see the _getPreferredHandlerID method.
+ *
+ * @param aHandlerApp {nsIHandlerApp} the handler app object
+ */
+ _getPossibleHandlerAppID: function HS__getPossibleHandlerAppID(aHandlerApp) {
+ var handlerAppID = "urn:handler:";
+
+ if (aHandlerApp instanceof Ci.nsILocalHandlerApp)
+ handlerAppID += "local:" + aHandlerApp.executable.path;
+ else {
+ aHandlerApp.QueryInterface(Ci.nsIWebHandlerApp);
+ handlerAppID += "web:" + aHandlerApp.uriTemplate;
+ }
+
+ return handlerAppID;
+ },
+
+ /**
* Get the list of types for the given class, creating the list if it doesn't
* already exist. The class can be either CLASS_MIMEINFO or CLASS_PROTOCOLINFO
* (i.e. the result of a call to _getClass).
*
* |urn:<class>s|
* |urn:<class>s:root|
*
* @param aClass {string} the class for which to retrieve a list of types
@@ -518,70 +616,147 @@ HandlerService.prototype = {
if (target instanceof Ci.nsIRDFLiteral)
return target.Value;
return null;
},
/**
+ * Get all targets for the property of an RDF source.
+ *
+ * @param sourceURI {string} the URI of the source
+ * @param propertyURI {string} the URI of the property
+ *
+ * @returns {nsISimpleEnumerator} an enumerator of targets
+ */
+ _getTargets: function HS__getTargets(sourceURI, propertyURI) {
+ var source = this._rdf.GetResource(sourceURI);
+ var property = this._rdf.GetResource(propertyURI);
+
+ return this._ds.GetTargets(source, property, true);
+ },
+
+ /**
* Set a property of an RDF source to a literal value.
*
* @param sourceURI {string} the URI of the source
* @param propertyURI {string} the URI of the property
* @param value {string} the literal value
*/
_setLiteral: function HS__setLiteral(sourceURI, propertyURI, value) {
var source = this._rdf.GetResource(sourceURI);
var property = this._rdf.GetResource(propertyURI);
var target = this._rdf.GetLiteral(value);
this._setTarget(source, property, target);
},
/**
- * Set a property of an RDF source to a resource.
+ * Set a property of an RDF source to a resource target.
*
* @param sourceURI {string} the URI of the source
* @param propertyURI {string} the URI of the property
- * @param resourceURI {string} the URI of the resource
+ * @param targetURI {string} the URI of the target
*/
- _setResource: function HS__setResource(sourceURI, propertyURI, resourceURI) {
+ _setResource: function HS__setResource(sourceURI, propertyURI, targetURI) {
var source = this._rdf.GetResource(sourceURI);
var property = this._rdf.GetResource(propertyURI);
- var target = this._rdf.GetResource(resourceURI);
+ var target = this._rdf.GetResource(targetURI);
this._setTarget(source, property, target);
},
/**
* Assert an arc into the RDF datasource if there is no arc with the given
* source and property; otherwise, if there is already an existing arc,
- * change it to point to the given target.
+ * change it to point to the given target. _setLiteral and _setResource
+ * call this after converting their string arguments into resources
+ * and literals, and most callers should call one of those two methods
+ * instead of this one.
*
* @param source {nsIRDFResource} the source
* @param property {nsIRDFResource} the property
- * @param value {nsIRDFNode} the target
+ * @param target {nsIRDFNode} the target
*/
_setTarget: function HS__setTarget(source, property, target) {
if (this._ds.hasArcOut(source, property)) {
var oldTarget = this._ds.GetTarget(source, property, true);
this._ds.Change(source, property, oldTarget, target);
}
else
this._ds.Assert(source, property, target, true);
},
/**
+ * Assert that a property of an RDF source has a resource target.
+ *
+ * The difference between this method and _setResource is that this one adds
+ * an assertion even if one already exists, which allows its callers to make
+ * sets of assertions (i.e. to set a property to multiple targets).
+ *
+ * @param sourceURI {string} the URI of the source
+ * @param propertyURI {string} the URI of the property
+ * @param targetURI {string} the URI of the target
+ */
+ _addResourceTarget: function HS__addResourceTarget(sourceURI, propertyURI,
+ targetURI) {
+ var source = this._rdf.GetResource(sourceURI);
+ var property = this._rdf.GetResource(propertyURI);
+ var target = this._rdf.GetResource(targetURI);
+
+ this._ds.Assert(source, property, target, true);
+ },
+
+ /**
+ * Whether or not a property of an RDF source has a given resource target.
+ *
+ * The difference between this method and _setResource is that this one adds
+ * an assertion even if one already exists, which allows its callers to make
+ * sets of assertions (i.e. to set a property to multiple targets).
+ *
+ * @param sourceURI {string} the URI of the source
+ * @param propertyURI {string} the URI of the property
+ * @param targetURI {string} the URI of the target
+ *
+ * @returns {boolean} whether or not there is such an assertion
+ */
+ _hasResourceTarget: function HS__hasResourceTarget(sourceURI, propertyURI,
+ targetURI) {
+ var source = this._rdf.GetResource(sourceURI);
+ var property = this._rdf.GetResource(propertyURI);
+ var target = this._rdf.GetResource(targetURI);
+
+ return this._ds.HasAssertion(source, property, target, true);
+ },
+
+ /**
+ * Whether or not there is an RDF source that has the given property set to
+ * the given resource target.
+ *
+ * @param propertyURI {string} the URI of the property
+ * @param targetURI {string} the URI of the target
+ *
+ * @returns {boolean} whether or not there is a source
+ */
+ _existsResourceTarget: function HS__existsResourceTarget(propertyURI,
+ targetURI) {
+ var property = this._rdf.GetResource(propertyURI);
+ var target = this._rdf.GetResource(targetURI);
+
+ return this._ds.hasArcIn(target, property);
+ },
+
+ /**
* Remove a property of an RDF source.
*
* @param sourceURI {string} the URI of the source
* @param propertyURI {string} the URI of the property
*/
- _removeValue: function HS__removeValue(sourceURI, propertyURI) {
+ _removeTarget: function HS__removeTarget(sourceURI, propertyURI) {
var source = this._rdf.GetResource(sourceURI);
var property = this._rdf.GetResource(propertyURI);
if (this._ds.hasArcOut(source, property)) {
var target = this._ds.GetTarget(source, property, true);
this._ds.Unassert(source, property, target);
}
},
--- a/uriloader/exthandler/tests/unit/test_handlerService.js
+++ b/uriloader/exthandler/tests/unit/test_handlerService.js
@@ -30,48 +30,77 @@
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
function run_test() {
+ //**************************************************************************//
+ // Constants
+
+ const handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].
+ getService(Ci.nsIHandlerService);
+
+ const mimeSvc = Cc["@mozilla.org/uriloader/external-helper-app-service;1"].
+ getService(Ci.nsIMIMEService);
+
+
+ //**************************************************************************//
+ // Sample Data
+
// It doesn't matter whether or not this nsIFile is actually executable,
// only that it has a path and exists. Since we don't know any executable
// that exists on all platforms (except possibly the application being
// tested, but there doesn't seem to be a way to get a reference to that
// from the directory service), we use the temporary directory itself.
var executable = HandlerServiceTest._dirSvc.get("TmpD", Ci.nsIFile);
+ // XXX We could, of course, create an actual executable in the directory:
+ //executable.append("localhandler");
+ //if (!executable.exists())
+ // executable.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0755);
var localHandler = {
name: "Local Handler",
executable: executable,
interfaces: [Ci.nsIHandlerApp, Ci.nsILocalHandlerApp, Ci.nsISupports],
QueryInterface: function(iid) {
if (!this.interfaces.some( function(v) { return iid.equals(v) } ))
throw Cr.NS_ERROR_NO_INTERFACE;
return this;
}
};
var webHandler = Cc["@mozilla.org/uriloader/web-handler-app;1"].
createInstance(Ci.nsIWebHandlerApp);
webHandler.name = "Web Handler";
webHandler.uriTemplate = "http://www.example.com/?%s";
-
- var handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].
- getService(Ci.nsIHandlerService);
-
- var mimeSvc = Cc["@mozilla.org/uriloader/external-helper-app-service;1"].
- getService(Ci.nsIMIMEService);
//**************************************************************************//
- // Default Properties
+ // Helper Functions
+
+ function checkLocalHandlersAreEquivalent(handler1, handler2) {
+ do_check_eq(handler1.name, handler2.name);
+ do_check_eq(handler1.executable.path, handler2.executable.path);
+ }
+
+ function checkWebHandlersAreEquivalent(handler1, handler2) {
+ do_check_eq(handler1.name, handler2.name);
+ do_check_eq(handler1.uriTemplate, handler2.uriTemplate);
+ }
+
+ // FIXME: these tests create and manipulate enough variables that it would
+ // make sense to move each test into its own scope so we don't run the risk
+ // of one test stomping on another's data.
+
+
+ //**************************************************************************//
+ // Test Default Properties
// Get a handler info for a MIME type that neither the application nor
// the OS knows about and make sure its properties are set to the proper
// default values.
var handlerInfo = mimeSvc.getFromTypeAndExtension("nonexistent/type", null);
// Make sure it's also an nsIHandlerInfo.
@@ -92,17 +121,17 @@ function run_test() {
do_check_eq(handlerInfo.description, "");
do_check_eq(handlerInfo.hasDefaultHandler, false);
do_check_eq(handlerInfo.defaultDescription, "");
// FIXME: test a default protocol handler.
//**************************************************************************//
- // Round-Trip Data Integrity
+ // Test Round-Trip Data Integrity
// Test round-trip data integrity by setting the properties of the handler
// info object to different values, telling the handler service to store the
// object, and then retrieving a new info object for the same type and making
// sure its properties are identical.
handlerInfo.preferredAction = Ci.nsIHandlerInfo.useHelperApp;
handlerInfo.preferredApplicationHandler = localHandler;
@@ -163,11 +192,75 @@ function run_test() {
removePreferredHandlerInfo =
mimeSvc.getFromTypeAndExtension("nonexistent/rem-preferred-handler", null);
removePreferredHandlerInfo.preferredApplicationHandler = null;
handlerSvc.store(removePreferredHandlerInfo);
removePreferredHandlerInfo =
mimeSvc.getFromTypeAndExtension("nonexistent/rem-preferred-handler", null);
do_check_eq(removePreferredHandlerInfo.preferredApplicationHandler, null);
+ // Make sure we can store and retrieve a handler info object with possible
+ // handlers. We test both adding and removing handlers.
+
+ // Get a handler info and make sure it has no possible handlers.
+ var possibleHandlersInfo =
+ mimeSvc.getFromTypeAndExtension("nonexistent/possible-handlers", null);
+ do_check_eq(possibleHandlersInfo.possibleApplicationHandlers.length, 0);
+
+ // Store and re-retrieve the handler and make sure it still has no possible
+ // handlers.
+ handlerSvc.store(possibleHandlersInfo);
+ possibleHandlersInfo =
+ mimeSvc.getFromTypeAndExtension("nonexistent/possible-handlers", null);
+ do_check_eq(possibleHandlersInfo.possibleApplicationHandlers.length, 0);
+
+ // Add two handlers, store the object, re-retrieve it, and make sure it has
+ // two handlers.
+ possibleHandlersInfo.possibleApplicationHandlers.appendElement(localHandler,
+ false);
+ possibleHandlersInfo.possibleApplicationHandlers.appendElement(webHandler,
+ false);
+ handlerSvc.store(possibleHandlersInfo);
+ possibleHandlersInfo =
+ mimeSvc.getFromTypeAndExtension("nonexistent/possible-handlers", null);
+ do_check_eq(possibleHandlersInfo.possibleApplicationHandlers.length, 2);
+
+ // Figure out which is the local and which is the web handler and the index
+ // in the array of the local handler, which is the one we're going to remove
+ // to test removal of a handler.
+ var handler1 = possibleHandlersInfo.possibleApplicationHandlers.
+ queryElementAt(0, Ci.nsIHandlerApp);
+ var handler2 = possibleHandlersInfo.possibleApplicationHandlers.
+ queryElementAt(1, Ci.nsIHandlerApp);
+ var localPossibleHandler, webPossibleHandler, localIndex;
+ if (handler1 instanceof Ci.nsILocalHandlerApp)
+ [localPossibleHandler, webPossibleHandler, localIndex] = [handler1,
+ handler2,
+ 0];
+ else
+ [localPossibleHandler, webPossibleHandler, localIndex] = [handler2,
+ handler1,
+ 1];
+ localPossibleHandler.QueryInterface(Ci.nsILocalHandlerApp);
+ webPossibleHandler.QueryInterface(Ci.nsIWebHandlerApp);
+
+ // Make sure the two handlers are the ones we stored.
+ checkLocalHandlersAreEquivalent(localPossibleHandler, localHandler);
+ checkWebHandlersAreEquivalent(webPossibleHandler, webHandler);
+
+ // Remove a handler, store the object, re-retrieve it, and make sure
+ // it only has one handler.
+ possibleHandlersInfo.possibleApplicationHandlers.removeElementAt(localIndex);
+ handlerSvc.store(possibleHandlersInfo);
+ possibleHandlersInfo =
+ mimeSvc.getFromTypeAndExtension("nonexistent/possible-handlers", null);
+ do_check_eq(possibleHandlersInfo.possibleApplicationHandlers.length, 1);
+
+ // Make sure the handler is the one we didn't remove.
+ checkWebHandlersAreEquivalent(possibleHandlersInfo.
+ possibleApplicationHandlers.
+ queryElementAt(0, Ci.nsIHandlerApp).
+ QueryInterface(Ci.nsIWebHandlerApp),
+ webHandler);
+
// FIXME: test round trip integrity for a protocol.
// FIXME: test round trip integrity for a handler info with a web handler.
}