Bug 589598 - InstallTrigger in iframes. r=Mossop, a=blocking2.0=betaN+
authorAlon Zakai <azakai@mozilla.com>
Thu, 09 Sep 2010 16:12:37 -0400
changeset 52326 1bd1a125cad9e9ec0517501c6493555246912657
parent 52325 4b50cff14c740a535ff30481ba078d8789fa69c1
child 52327 89816be9e16417c6f5f7d3a19600a17a9d5775f4
push id1
push userroot
push dateTue, 26 Apr 2011 22:38:44 +0000
treeherdermozilla-beta@bfdb6e623a36 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMossop, blocking2
bugs589598
milestone2.0b6pre
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 589598 - InstallTrigger in iframes. r=Mossop, a=blocking2.0=betaN+
toolkit/mozapps/extensions/content/extensions-content.js
toolkit/mozapps/extensions/test/xpinstall/Makefile.in
toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_trigger_iframe.js
toolkit/mozapps/extensions/test/xpinstall/installtrigger_frame.html
--- a/toolkit/mozapps/extensions/content/extensions-content.js
+++ b/toolkit/mozapps/extensions/content/extensions-content.js
@@ -43,18 +43,17 @@ const Cu = Components.utils;
 
 const MSG_INSTALL_ENABLED  = "WebInstallerIsInstallEnabled";
 const MSG_INSTALL_ADDONS   = "WebInstallerInstallAddonsFromWebpage";
 const MSG_INSTALL_CALLBACK = "WebInstallerInstallCallback";
 
 var gIoService = Components.classes["@mozilla.org/network/io-service;1"]
                            .getService(Components.interfaces.nsIIOService);
 
-function InstallTrigger(installerId, window) {
-  this.installerId = installerId;
+function InstallTrigger(window) {
   this.window = window;
 }
 
 InstallTrigger.prototype = {
   __exposedProps__: {
     SKIN: "r",
     LOCALE: "r",
     CONTENT: "r",
@@ -125,17 +124,17 @@ InstallTrigger.prototype = {
         }
       }
       params.uris.push(url.spec);
       params.hashes.push("Hash" in item ? item.Hash : null);
       params.names.push(name);
       params.icons.push(iconUrl ? iconUrl.spec : null);
     }
     // Add callback Id, done here, so only if we actually got here
-    params.callbackId = this.addCallback(aCallback, params.uris);
+    params.callbackId = manager.addCallback(aCallback, params.uris);
     // Send message
     return sendSyncMessage(MSG_INSTALL_ADDONS, params)[0];
   },
 
   /**
    * @see amIInstallTriggerInstaller.idl
    */
   startSoftwareUpdate: function(aUrl, aFlags) {
@@ -148,49 +147,16 @@ InstallTrigger.prototype = {
 
   /**
    * @see amIInstallTriggerInstaller.idl
    */
   installChrome: function(aType, aUrl, aSkin) {
     return this.startSoftwareUpdate(aUrl);
   },
 
-  // == Internal, hidden machinery ==
-
-  callbacks: {},
-
-  /**
-   * Adds a callback to the list of callbacks we may receive messages
-   * about from the parent process. We save them here; only callback IDs
-   * are sent over IPC.
-   *
-   * @param  callback
-   *         The callback function
-   * @param  urls
-   *         The urls this callback function will receive responses for.
-   *         After all the callbacks have arrived, we can forget about the
-   *         callback.
-   *
-   * @return The callback ID, an integer identifying this callback.
-   */
-  addCallback: function(aCallback, aUrls) {
-    if (!aCallback)
-      return -1;
-    var callbackId = 0;
-    while (callbackId in this.callbacks)
-      callbackId++;
-    this.callbacks[callbackId] = {
-      callback: aCallback,
-      urls: aUrls.slice(0), // Clone the urls for our own use (it lets
-                            // us know when no further callbacks will
-                            // occur)
-    };
-    return callbackId;
-  },
-
   /**
    * Resolves a URL in the context of our current window. We need to do
    * this before sending URLs to the parent process.
    *
    * @param  aUrl
    *         The url to resolve.
    *
    * @return A resolved, absolute nsURI object.
@@ -218,100 +184,114 @@ InstallTrigger.prototype = {
       return false;
     }
   },
 };
 
 /**
  * Child part of InstallTrigger e10s handling.
  *
- * Sets up InstallTriggers on newly-created windows,
+ * Sets up InstallTrigger for newly-created windows,
  * that will relay messages for InstallTrigger
  * activity. We also process the parameters for
  * the InstallTrigger to proper parameters for
  * amIWebInstaller.
  */
 function InstallTriggerManager() {
-  this.installerIds = [];
-  this.nextInstallerId = 0;
+  this.callbacks = {};
 
   addMessageListener(MSG_INSTALL_CALLBACK, this);
 
   addEventListener("DOMWindowCreated", this, false);
 
   var self = this;
   addEventListener("unload", function() {
     // Clean up all references, to help gc work quickly
-    for (var installerId in self.installerIds) {
-      self.installerIds[installerId].callbacks = null;
-      self.installerIds[installerId] = null;
-    }
-    self.installerIds = null;
+    self.callbacks = null;
   }, false);
 }
 
 InstallTriggerManager.prototype = {
   handleEvent: function handleEvent(aEvent) {
-    var window = aEvent.originalTarget.defaultView.content;
+    var window = aEvent.target.defaultView;
 
     // Need to make sure we are called on what we care about -
     // content windows. DOMWindowCreated is called on *all* HTMLDocuments,
-    // some of which belong to ChromeWindows or lack defaultView.content
-    // altogether.
+    // some of which belong to chrome windows or other special content.
     //
-    // Note about the syntax used here: |"wrappedJSObject" in window|
-    // will silently fail, without even letting us catch it as an
-    // exception, and checking in the way that we do check in some
-    // cases still throws an exception; see bug 582108 about both.
-    try {
-      if (!window || !window.wrappedJSObject) {
-        return;
-      }
-    }
-    catch(e) {
+    var uri = window.document.documentURIObject;
+    if (uri.scheme === "chrome" || uri.spec.split(":")[0] == "about") {
       return;
     }
 
-    // This event happens for each HTMLDocument, so it can happen more than
-    // once per Window. We only need to work once per Window though.
-    if (window.wrappedJSObject.InstallTrigger)
-        return;
+    window.wrappedJSObject.__defineGetter__("InstallTrigger", this.createInstallTrigger);
+  },
+
+  createInstallTrigger: function createInstallTrigger() {
+    // 'this' is the window itself. We do this in a getter, so that
+    // we create these objects only on demand (this is a potential
+    // concern, since otherwise we might add one per iframe, and
+    // keep them alive for as long as the tab is alive).
+    delete this.InstallTrigger; // remove getter
+    this.InstallTrigger = new InstallTrigger(this);
+    return this.InstallTrigger;
+  },
 
-    // Create the public object which web scripts can see
-    var installerId = this.nextInstallerId ++;
-    var installTrigger = new InstallTrigger(installerId, window);
-    this.installerIds[installerId] = installTrigger;
-    window.wrappedJSObject.InstallTrigger = installTrigger;
+  /**
+   * Adds a callback to the list of callbacks we may receive messages
+   * about from the parent process. We save them here; only callback IDs
+   * are sent over IPC.
+   *
+   * @param  callback
+   *         The callback function
+   * @param  urls
+   *         The urls this callback function will receive responses for.
+   *         After all the callbacks have arrived, we can forget about the
+   *         callback.
+   *
+   * @return The callback ID, an integer identifying this callback.
+   */
+  addCallback: function(aCallback, aUrls) {
+    if (!aCallback)
+      return -1;
+    var callbackId = 0;
+    while (callbackId in this.callbacks)
+      callbackId++;
+    this.callbacks[callbackId] = {
+      callback: aCallback,
+      urls: aUrls.slice(0), // Clone the urls for our own use (it lets
+                            // us know when no further callbacks will
+                            // occur)
+    };
+    return callbackId;
   },
 
   /**
    * Receives a message about a callback. Performs the actual callback
    * (for the callback with the ID we are given). When
    * all URLs are exhausted, can free the callbackId and linked stuff.
    *
    * @param  message
-   *         The IPC message. Contains IDs of the installer and the
-   *         callback.
+   *         The IPC message. Contains the callback ID.
    *
    */
   receiveMessage: function(aMessage) {
     var payload = aMessage.json;
-    var installer = this.installerIds[payload.installerId];
     var callbackId = payload.callbackId;
     var url = payload.url;
     var status = payload.status;
-    var callbackObj = installer.callbacks[callbackId];
+    var callbackObj = this.callbacks[callbackId];
     if (!callbackObj)
       return;
     try {
       callbackObj.callback(url, status);
     }
     catch (e) {
       dump("InstallTrigger callback threw an exception: " + e + "\n");
     }
     callbackObj.urls.splice(callbackObj.urls.indexOf(url), 1);
     if (callbackObj.urls.length == 0)
-      installer.callbacks[callbackId] = null;
+      this.callbacks[callbackId] = null;
   },
 };
 
-new InstallTriggerManager();
+var manager = new InstallTriggerManager();
 
--- a/toolkit/mozapps/extensions/test/xpinstall/Makefile.in
+++ b/toolkit/mozapps/extensions/test/xpinstall/Makefile.in
@@ -41,16 +41,17 @@ VPATH		= @srcdir@
 relativesrcdir  = toolkit/mozapps/extensions/test/xpinstall
 
 include $(DEPTH)/config/autoconf.mk
 include $(topsrcdir)/config/rules.mk
 
 _BROWSER_FILES = head.js \
                  browser_unsigned_url.js \
                  browser_unsigned_trigger.js \
+                 browser_unsigned_trigger_iframe.js \
                  browser_whitelist.js \
                  browser_whitelist2.js \
                  browser_whitelist3.js \
                  browser_whitelist4.js \
                  browser_whitelist5.js \
                  browser_whitelist6.js \
                  browser_hash.js \
                  browser_badhash.js \
@@ -100,16 +101,17 @@ include $(topsrcdir)/config/rules.mk
                  theme.xpi \
                  restartless.xpi \
                  incompatible.xpi \
                  empty.xpi \
                  corrupt.xpi \
                  multipackage.xpi \
                  enabled.html \
                  installtrigger.html \
+                 installtrigger_frame.html \
                  startsoftwareupdate.html \
                  installchrome.html \
                  triggerredirect.html \
                  authRedirect.sjs \
                  cookieRedirect.sjs \
                  hashRedirect.sjs \
                  bug540558.html \
                  $(NULL)
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_trigger_iframe.js
@@ -0,0 +1,50 @@
+// ----------------------------------------------------------------------------
+// Test for bug 589598 - Ensure that installing through InstallTrigger
+// works in an iframe in web content.
+
+function test() {
+  Harness.installConfirmCallback = confirm_install;
+  Harness.installEndedCallback = install_ended;
+  Harness.installsCompletedCallback = finish_test;
+  Harness.setup();
+
+  var pm = Services.perms;
+  pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+  var triggers = encodeURIComponent(JSON.stringify({
+    "Unsigned XPI": {
+      URL: TESTROOT + "unsigned.xpi",
+      IconURL: TESTROOT + "icon.png",
+      toString: function() { return this.URL; }
+    }
+  }));
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.loadURI(TESTROOT + "installtrigger_frame.html?" + triggers);
+}
+
+function confirm_install(window) {
+  items = window.document.getElementById("itemList").childNodes;
+  is(items.length, 1, "Should only be 1 item listed in the confirmation dialog");
+  is(items[0].name, "XPI Test", "Should have seen the name");
+  is(items[0].url, TESTROOT + "unsigned.xpi", "Should have listed the correct url for the item");
+  is(items[0].icon, TESTROOT + "icon.png", "Should have listed the correct icon for the item");
+  is(items[0].signed, "false", "Should have listed the item as unsigned");
+  return true;
+}
+
+function install_ended(install, addon) {
+  install.cancel();
+}
+
+function finish_test(count) {
+  is(count, 1, "1 Add-on should have been successfully installed");
+
+  Services.perms.remove("example.com", "install");
+
+  var doc = gBrowser.contentWindow.frames[0].document; // Document of iframe
+  is(doc.getElementById("return").textContent, "true", "installTrigger in iframe should have claimed success");
+  is(doc.getElementById("status").textContent, "0", "Callback in iframe should have seen a success");
+
+  gBrowser.removeCurrentTab();
+  Harness.finish();
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/installtrigger_frame.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+          "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+<html>
+
+<!-- This page will accept some json as the uri query and pass it to
+     an inner iframe, which will run InstallTrigger.install -->
+
+<head>
+<title>InstallTrigger frame tests</title>
+<script type="text/javascript">
+function prepChild() {
+  // Pass our parameters over to the child
+  var child = window.frames[0];
+  var params = document.location.search.substr(1);
+  child.location = "installtrigger.html?" + params;
+}
+</script>
+</head>
+<body onload="prepChild()">
+
+<iframe src="about:blank">
+</iframe>
+
+<p>InstallTrigger tests</p>
+<p id="return"></p>
+<p id="status"></p>
+</body>
+</html>