Bug 1007982 - Add shim for adblock about: protocol (r=mconley)
authorBill McCloskey <wmccloskey@mozilla.com>
Fri, 18 Jul 2014 08:59:00 -0700
changeset 216860 9f9ac2060b54c84a8fa9e83699d2b11e67a97612
parent 216859 be9e86108cf95b31f73c1fc1cf7b8370f1b173c3
child 216861 e1abce6fa2c1c427306ddf87f5d9c57e80530ae7
push id515
push userraliiev@mozilla.com
push dateMon, 06 Oct 2014 12:51:51 +0000
treeherdermozilla-release@267c7a481bef [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmconley
bugs1007982
milestone33.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1007982 - Add shim for adblock about: protocol (r=mconley)
toolkit/components/addoncompat/RemoteAddonsChild.jsm
toolkit/components/addoncompat/RemoteAddonsParent.jsm
--- a/toolkit/components/addoncompat/RemoteAddonsChild.jsm
+++ b/toolkit/components/addoncompat/RemoteAddonsChild.jsm
@@ -2,20 +2,27 @@
 // License, v. 2.0. If a copy of the MPL was not distributed with this
 // file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 this.EXPORTED_SYMBOLS = ["RemoteAddonsChild"];
 
 const Ci = Components.interfaces;
 const Cc = Components.classes;
 const Cu = Components.utils;
+const Cr = Components.results;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
+                                  "resource://gre/modules/BrowserUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "SystemPrincipal",
+                                   "@mozilla.org/systemprincipal;1", "nsIPrincipal");
+
 // Similar to Python. Returns dict[key] if it exists. Otherwise,
 // sets dict[key] to default_ and returns default_.
 function setDefault(dict, key, default_)
 {
   if (key in dict) {
     return dict[key];
   }
   dict[key] = default_;
@@ -107,19 +114,19 @@ let ContentPolicyChild = {
   },
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPolicy, Ci.nsIObserver,
                                          Ci.nsIChannelEventSink, Ci.nsIFactory,
                                          Ci.nsISupportsWeakReference]),
 
   track: function(path, count) {
     let catMan = Cc["@mozilla.org/categorymanager;1"].getService(Ci.nsICategoryManager);
-    if (count) {
+    if (count == 1) {
       catMan.addCategoryEntry("content-policy", this._contractID, this._contractID, false, true);
-    } else {
+    } else if (count == 0) {
       catMan.deleteCategoryEntry("content-policy", this._contractID, false);
     }
   },
 
   shouldLoad: function(contentType, contentLocation, requestOrigin, node, mimeTypeGuess, extra) {
     let cpmm = Cc["@mozilla.org/childprocessmessagemanager;1"]
                .getService(Ci.nsISyncMessageSender);
     var rval = cpmm.sendRpcMessage("Addons:ContentPolicy:Run", {}, {
@@ -143,28 +150,180 @@ let ContentPolicyChild = {
   createInstance: function(outer, iid) {
     if (outer) {
       throw Cr.NS_ERROR_NO_AGGREGATION;
     }
     return this.QueryInterface(iid);
   },
 };
 
+// This is a shim channel whose only purpose is to return some string
+// data from an about: protocol handler.
+function AboutProtocolChannel(data, uri, originalURI, contentType)
+{
+  let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream);
+  stream.setData(data, data.length);
+  this._stream = stream;
+
+  this.URI = BrowserUtils.makeURI(uri);
+  this.originalURI = BrowserUtils.makeURI(originalURI);
+  this.contentType = contentType;
+}
+
+AboutProtocolChannel.prototype = {
+  contentCharset: "utf-8",
+  contentLength: 0,
+  owner: SystemPrincipal,
+  securityInfo: null,
+  notificationCallbacks: null,
+  loadFlags: 0,
+  loadGroup: null,
+  name: null,
+  status: Cr.NS_OK,
+
+  asyncOpen: function(listener, context) {
+    let runnable = {
+      run: () => {
+        try {
+          listener.onStartRequest(this, context);
+        } catch(e) {}
+        try {
+          listener.onDataAvailable(this, context, this._stream, 0, this._stream.available());
+        } catch(e) {}
+        try {
+          listener.onStopRequest(this, context, Cr.NS_OK);
+        } catch(e) {}
+      }
+    };
+    Services.tm.currentThread.dispatch(runnable, Ci.nsIEventTarget.DISPATCH_NORMAL);
+  },
+
+  open: function() {
+    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+  },
+
+  isPending: function() {
+    return false;
+  },
+
+  cancel: function() {
+    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+  },
+
+  suspend: function() {
+    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+  },
+
+  resume: function() {
+    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+  },
+
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIChannel, Ci.nsIRequest])
+};
+
+// This shim protocol handler is used when content fetches an about: URL.
+function AboutProtocolInstance(contractID)
+{
+  this._contractID = contractID;
+  this._uriFlags = null;
+}
+
+AboutProtocolInstance.prototype = {
+  createInstance: function(outer, iid) {
+    if (outer != null) {
+      throw Cr.NS_ERROR_NO_AGGREGATION;
+    }
+
+    return this.QueryInterface(iid);
+  },
+
+  getURIFlags: function(uri) {
+    // Cache the result to avoid the extra IPC.
+    if (this._uriFlags !== undefined) {
+      return this._uriFlags;
+    }
+
+    let cpmm = Cc["@mozilla.org/childprocessmessagemanager;1"]
+               .getService(Ci.nsISyncMessageSender);
+
+    var rval = cpmm.sendRpcMessage("Addons:AboutProtocol:GetURIFlags", {
+      uri: uri.spec,
+      contractID: this._contractID
+    });
+
+    if (rval.length != 1) {
+      throw Cr.NS_ERROR_FAILURE;
+    }
+
+    this._uriFlags = rval[0];
+    return this._uriFlags;
+  },
+
+  // We take some shortcuts here. Ideally, we would return a CPOW that
+  // wraps the add-on's nsIChannel. However, many of the methods
+  // related to nsIChannel are marked [noscript], so they're not
+  // available to CPOWs. Consequently, the parent simply reads all the
+  // data out of the add-on's channel and returns that as a string. We
+  // create a new AboutProtocolChannel whose only purpose is to return
+  // the string data via an nsIStringInputStream.
+  newChannel: function(uri) {
+    let cpmm = Cc["@mozilla.org/childprocessmessagemanager;1"]
+               .getService(Ci.nsISyncMessageSender);
+
+    var rval = cpmm.sendRpcMessage("Addons:AboutProtocol:NewChannel", {
+      uri: uri.spec,
+      contractID: this._contractID
+    });
+
+    if (rval.length != 1) {
+      throw Cr.NS_ERROR_FAILURE;
+    }
+
+    let {data, uri, originalURI, contentType} = rval[0];
+    return new AboutProtocolChannel(data, uri, originalURI, contentType);
+  },
+
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIFactory, Ci.nsIAboutModule])
+};
+
+let AboutProtocolChild = {
+  _classDescription: "Addon shim about: protocol handler",
+  _classID: Components.ID("8d56a310-0c80-11e4-9191-0800200c9a66"),
+
+  init: function() {
+    this._instances = {};
+    NotificationTracker.watch("about-protocol", (path, count) => this.track(path, count));
+  },
+
+  track: function(path, count) {
+    let contractID = path[1];
+    let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+    if (count == 1) {
+      let instance = new AboutProtocolInstance(contractID);
+      this._instances[contractID] = instance;
+      registrar.registerFactory(this._classID, this._classDescription, contractID, instance);
+    } else if (count == 0) {
+      delete this._instances[contractID];
+      registerFactory.unregisterFactory(this._classID, this);
+    }
+  },
+};
+
 // This code registers observers in the child whenever an add-on in
 // the parent asks for notifications on the given topic.
 let ObserverChild = {
   init: function() {
     NotificationTracker.watch("observer", (path, count) => this.track(path, count));
   },
 
   track: function(path, count) {
     let topic = path[1];
-    if (count) {
+    if (count == 1) {
       Services.obs.addObserver(this, topic, false);
-    } else {
+    } else if (count == 0) {
       Services.obs.removeObserver(this, topic);
     }
   },
 
   observe: function(subject, topic, data) {
     let cpmm = Cc["@mozilla.org/childprocessmessagemanager;1"]
                .getService(Ci.nsISyncMessageSender);
     cpmm.sendRpcMessage("Addons:Observer:Run", {}, {
@@ -183,19 +342,19 @@ function EventTargetChild(childGlobal)
   this._childGlobal = childGlobal;
   NotificationTracker.watch("event", (path, count) => this.track(path, count));
 }
 
 EventTargetChild.prototype = {
   track: function(path, count) {
     let eventType = path[1];
     let useCapture = path[2];
-    if (count) {
+    if (count == 1) {
       this._childGlobal.addEventListener(eventType, this, useCapture, true);
-    } else {
+    } else if (count == 0) {
       this._childGlobal.removeEventListener(eventType, this, useCapture);
     }
   },
 
   handleEvent: function(event) {
     this._childGlobal.sendRpcMessage("Addons:Event:Run",
                                      {type: event.type, isTrusted: event.isTrusted},
                                      {event: event});
@@ -248,16 +407,17 @@ SandboxChild.prototype = {
 };
 
 let RemoteAddonsChild = {
   _ready: false,
 
   makeReady: function() {
     NotificationTracker.init();
     ContentPolicyChild.init();
+    AboutProtocolChild.init();
     ObserverChild.init();
   },
 
   init: function(global) {
     if (!this._ready) {
       this.makeReady();
       this._ready = true;
     }
--- a/toolkit/components/addoncompat/RemoteAddonsParent.jsm
+++ b/toolkit/components/addoncompat/RemoteAddonsParent.jsm
@@ -6,16 +6,21 @@ this.EXPORTED_SYMBOLS = ["RemoteAddonsPa
 
 const Ci = Components.interfaces;
 const Cc = Components.classes;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import('resource://gre/modules/Services.jsm');
 
+XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
+                                  "resource://gre/modules/BrowserUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+                                  "resource://gre/modules/NetUtil.jsm");
+
 // Similar to Python. Returns dict[key] if it exists. Otherwise,
 // sets dict[key] to default_ and returns default_.
 function setDefault(dict, key, default_)
 {
   if (key in dict) {
     return dict[key];
   }
   dict[key] = default_;
@@ -167,16 +172,109 @@ CategoryManagerInterposition.methods.del
   function(addon, target, category, entry, persist) {
     if (category == "content-policy") {
       ContentPolicyParent.remoteContentPolicy(entry);
     }
 
     target.deleteCategoryEntry(category, entry, persist);
   };
 
+// This shim handles the case where an add-on registers an about:
+// protocol handler in the parent and we want the child to be able to
+// use it. This code is pretty specific to Adblock's usage.
+let AboutProtocolParent = {
+  init: function() {
+    let ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"]
+               .getService(Ci.nsIMessageBroadcaster);
+    ppmm.addMessageListener("Addons:AboutProtocol:GetURIFlags", this);
+    ppmm.addMessageListener("Addons:AboutProtocol:NewChannel", this);
+    this._protocols = [];
+  },
+
+  registerFactory: function(class_, className, contractID, factory) {
+    this._protocols.push({contractID: contractID, factory: factory});
+    NotificationTracker.add(["about-protocol", contractID]);
+  },
+
+  unregisterFactory: function(class_, factory) {
+    for (let i = 0; i < this._protocols.length; i++) {
+      if (this._protocols[i].factory == factory) {
+        NotificationTracker.remove(["about-protocol", this._protocols[i].contractID]);
+        this._protocols.splice(i, 1);
+        break;
+      }
+    }
+  },
+
+  receiveMessage: function (msg) {
+    switch (msg.name) {
+      case "Addons:AboutProtocol:GetURIFlags":
+        return this.getURIFlags(msg);
+      case "Addons:AboutProtocol:NewChannel":
+        return this.newChannel(msg);
+        break;
+    }
+  },
+
+  getURIFlags: function(msg) {
+    let uri = BrowserUtils.makeURI(msg.data.uri);
+    let contractID = msg.data.contractID;
+    let module = Cc[contractID].getService(Ci.nsIAboutModule);
+    try {
+      return module.getURIFlags(uri);
+    } catch (e) {
+      Cu.reportError(e);
+    }
+  },
+
+  // We take some shortcuts here. Ideally, we would return a CPOW that
+  // wraps the add-on's nsIChannel. However, many of the methods
+  // related to nsIChannel are marked [noscript], so they're not
+  // available to CPOWs. Consequently, we immediately read all the
+  // data out of the channel here and pass it to the child. The child
+  // then returns a shim channel that wraps an nsIStringInputStream
+  // for the string we read.
+  newChannel: function(msg) {
+    let uri = BrowserUtils.makeURI(msg.data.uri);
+    let contractID = msg.data.contractID;
+    let module = Cc[contractID].getService(Ci.nsIAboutModule);
+    try {
+      let channel = module.newChannel(uri);
+      let stream = channel.open();
+      let data = NetUtil.readInputStreamToString(stream, stream.available(), {});
+      return {
+        data: data,
+        uri: channel.URI.spec,
+        originalURI: channel.originalURI.spec,
+        contentType: channel.contentType
+      };
+    } catch (e) {
+      Cu.reportError(e);
+    }
+  },
+};
+AboutProtocolParent.init();
+
+let ComponentRegistrarInterposition = new Interposition();
+
+ComponentRegistrarInterposition.methods.registerFactory =
+  function(addon, target, class_, className, contractID, factory) {
+    if (contractID.startsWith("@mozilla.org/network/protocol/about;1?")) {
+      AboutProtocolParent.registerFactory(class_, className, contractID, factory);
+    }
+
+    target.registerFactory(class_, className, contractID, factory);
+  };
+
+ComponentRegistrarInterposition.methods.unregisterFactory =
+  function(addon, target, class_, factory) {
+    AboutProtocolParent.tryUnregisterFactory(class_, factory);
+    target.unregisterFactory(class_, factory);
+  };
+
 // This object manages add-on observers that might fire in the child
 // process. Rather than managing the observers itself, it uses the
 // parent's observer service. When an add-on listens on topic T,
 // ObserverParent asks the child process to listen on T. It also adds
 // an observer in the parent for the topic e10s-T. When the T observer
 // fires in the child, the parent fires all the e10s-T observers,
 // passing them CPOWs for the subject and data. We don't want to use T
 // in the parent because there might be non-add-on T observers that
@@ -544,16 +642,17 @@ let RemoteAddonsParent = {
   getInterfaceInterpositions: function() {
     let result = {};
 
     function register(intf, interp) {
       result[intf.number] = interp;
     }
 
     register(Ci.nsICategoryManager, CategoryManagerInterposition);
+    register(Ci.nsIComponentRegistrar, ComponentRegistrarInterposition);
     register(Ci.nsIObserverService, ObserverInterposition);
     register(Ci.nsIXPCComponents_Utils, ComponentsUtilsInterposition);
 
     return result;
   },
 
   getTaggedInterpositions: function() {
     let result = {};