Bug 718486 - Make Account Provisioner XML handler only request XML once. r=bienvenu, a=Standard8.
authorMike Conley <mconley@mozilla.com>
Fri, 27 Jan 2012 18:13:21 -0500
changeset 10135 070572a7da3de6da3fc0c3883cf4bc38b0ecf180
parent 10134 b808530327a54771ac180368e824ac6e23b61b91
child 10136 5499067065fea1ec515ff5e1c152b4fc2abc4dad
push idunknown
push userunknown
push dateunknown
reviewersbienvenu, Standard8
bugs718486
Bug 718486 - Make Account Provisioner XML handler only request XML once. r=bienvenu, a=Standard8.
mail/components/newmailaccount/content/accountProvisioner.js
mail/components/newmailaccount/content/uriListener.js
--- a/mail/components/newmailaccount/content/accountProvisioner.js
+++ b/mail/components/newmailaccount/content/accountProvisioner.js
@@ -43,17 +43,17 @@ let Cc = Components.classes;
 let Ci = Components.interfaces;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
 Cu.import("resource://gre/modules/PluralForm.jsm");
 Cu.import("resource:///modules/StringBundle.js");
 Cu.import("resource:///modules/mailServices.js");
-Cu.import("resource:///modules/gloda/log4moz.js");
+Cu.import("resource:///modules/gloda/log4moz.js");
 
 // Get a configured logger for this component.
 // To debug, set mail.provider.logging.dump (or .console)="All"
 let gLog = Log4Moz.getConfiguredLogger("mail.provider");
 let stringBundle = new StringBundle("chrome://messenger/locale/newmailaccount/accountProvisioner.properties");
 
 let isOSX = (Services.appinfo.OS == 'Darwin');
 
@@ -67,17 +67,17 @@ function isAccel (event) (isOSX && event
  *
  * Cribbed from
  *   mozilla/dom/tests/mochitest/localstorage/test_localStorageFromChrome.xhtml
  *
  * @param {String} page The page to get the localstorage for.
  * @return {nsIDOMStorage} The localstorage for this page.
  */
 function getLocalStorage(page) {
-  var url = "http://example.com/" + page;
+  var url = "chrome://content/messenger/accountProvisionerStorage/" + page;
   var ssm = Cc["@mozilla.org/scriptsecuritymanager;1"]
     .getService(Ci.nsIScriptSecurityManager);
   var dsm = Cc["@mozilla.org/dom/storagemanager;1"]
     .getService(Ci.nsIDOMStorageManager);
 
   var uri = Services.io.newURI(url, "", null);
   var principal = ssm.getCodebasePrincipal(uri);
   return dsm.getLocalStorageForPrincipal(principal, url);
@@ -452,36 +452,83 @@ var EmailAccountProvisioner = {
               name + "=" + encodeURIComponent(data[name]);
     }
 
     gLog.info("Opening up a contentTab with the order form.");
     // Then open a content tab.
     let mail3Pane = Cc["@mozilla.org/appshell/window-mediator;1"]
           .getService(Ci.nsIWindowMediator)
           .getMostRecentWindow("mail:3pane");
+
     let tabmail = mail3Pane.document.getElementById("tabmail");
     tabmail.openTab("contentTab", {
       contentPage: url,
-      onListener: function(aBrowser, aListener) {
-        // We're passing the value of search_engine to the listener so that when
-        // we reopen that window, we don't have to wait for the re-parsing of
-        // the provider list to figure out what is the search engine name for
-        // that provider.
-        let progressListener = new mail3Pane.AccountProvisionerListener(
-          aBrowser, {
-            realName: firstName + " " + lastName,
-            email: email,
-            searchEngine: provider.search_engine,
-          });
-        aListener.addProgressListener(progressListener);
-      },
-      onLoad: function (event, aBrowser) {
+      onLoad: function (aEvent, aBrowser) {
+        // There are a few things we want to do once the tab content loads:
+        // 1.  We want to register an observer to watch for HTTP requests
+        //     where the contentType contains text/xml
+        // 2.  We want to register a tab monitor to watch for when the
+        //     tab we're opening closes, so that it can clean up the
+        //     observer.
+
+        // At this point, when onLoad is called, we run into some scoping
+        // issues.  Services and gLog are no longer in scope, so we have to
+        // redefine them.
+        Components.utils.import("resource://gre/modules/Services.jsm");
+        Components.utils.import("resource:///modules/gloda/log4moz.js");
+        let gLog = Log4Moz.getConfiguredLogger("mail.provider");
+
+        // We'll construct our special observer (defined in urlListener.js)
+        // that will watch for requests where the contentType contains
+        // text/xml.
+        let observer = new mail3Pane.httpRequestObserver(aBrowser, {
+          realName: firstName + " " + lastName,
+          email: email,
+          searchEngine: provider.search_engine,
+        });
+
+        // Register our observer
+        Services.obs.addObserver(observer, "http-on-examine-response",
+                                 false);
+        gLog.info("httpRequestObserver wired up.");
+
+        // The provisionerTabMonitor lets us clean up when the tab closes.
+        // This tab closure can occur from a variety of events (successful
+        // account transaction, user closes the tab, user quits Thunderbird,
+        // etc), and so the tab monitor allows us to catch all of those
+        // cases.
+        let provisionerTabMonitor = {
+          monitorName: "accountProvisionerMonitor",
+          onTabTitleChanged: function() {},
+          onTabSwitched: function(aTab, aOldTab) {},
+          onTabOpened: function(aTab, aIsFirstTab, aWasCurrentTab) {},
+          onTabClosing: function(aTab) {
+            if (aTab.browser === aBrowser) {
+              // Once again, due to scoping issues, we have to re-import
+              // Services.
+              Components.utils.import("resource://gre/modules/Services.jsm");
+              gLog.info("Performing account provisioner cleanup");
+              gLog.info("Unregistering tab monitor");
+              tabmail.unregisterTabMonitor(provisionerTabMonitor);
+              gLog.info("Removing httpRequestObserver");
+              Services.obs.removeObserver(observer, "http-on-examine-response");
+              gLog.info("Account provisioner cleanup is done.");
+            }
+          },
+          onTabPersist: function(aTab) {},
+          onTabRestored: function(aTab, aState, aIsFirstTab) {},
+        }
+
+        // Register the monitor.
+        tabmail.registerTabMonitor(provisionerTabMonitor);
+
         // Close the Account Provisioner window once the page
         // has loaded.
-        gLog.info("Handing off to the contentTab, and closing Email Account Provisioner.");
+        gLog.info("Handing off to the contentTab, and closing Email "
+                  + "Account Provisioner.")
         window.close();
       },
     });
     // Wait for the handler to close us.
     EmailAccountProvisioner.spinning(true);
     EmailAccountProvisioner.searchEnabled(false);
     $("#notifications").children().not(".spinner").hide();
   },
--- a/mail/components/newmailaccount/content/uriListener.js
+++ b/mail/components/newmailaccount/content/uriListener.js
@@ -59,122 +59,201 @@ Services.scriptloader.loadSubScript("chr
 Services.scriptloader.loadSubScript("chrome://messenger/content/accountcreation/fetchhttp.js", accountCreationFuncs);
 Services.scriptloader.loadSubScript("chrome://messenger/content/accountcreation/readFromXML.js", accountCreationFuncs);
 Services.scriptloader.loadSubScript("chrome://messenger/content/accountcreation/verifyConfig.js", accountCreationFuncs);
 Services.scriptloader.loadSubScript("chrome://messenger/content/accountcreation/fetchConfig.js", accountCreationFuncs);
 Services.scriptloader.loadSubScript("chrome://messenger/content/accountcreation/createInBackend.js", accountCreationFuncs);
 Services.scriptloader.loadSubScript("chrome://messenger/content/accountcreation/MyBadCertHandler.js", accountCreationFuncs);
 
 /**
- * This is a listener that will take care of intercepting the right request and
- * creating the account accordingly.
+ * This is an observer that watches all HTTP requests for one where the
+ * response contentType contains text/xml.  Once that observation is
+ * made, we ensure that the associated window for that request matches
+ * the window belonging to the content tab for the account order form.
+ * If so, we attach an nsITraceableListener to read the contents of the
+ * request response, and react accordingly if the contents can be turned
+ * into an email account.
  *
  * @param aBrowser The XUL <browser> the request lives in.
  * @param aParams An object containing various bits of information.
  * @param aParams.realName The real name of the person
  * @param aParams.email The email address the person picked.
  * @param aParams.searchEngine The search engine associated to that provider.
  */
-function AccountProvisionerListener (aBrowser, aParams) {
+function httpRequestObserver(aBrowser, aParams) {
   this.browser = aBrowser;
   this.params = aParams;
 }
 
-AccountProvisionerListener.prototype = {
-  onStateChange: function (/* in nsIWebProgress */ aWebProgress,
-                           /* in nsIRequest */ aRequest,
-                           /* in unsigned long */ aStateFlags,
-                           /* in nsresult */ aStatus) {
-    // This is the earliest notification we get...
-    if ((aStateFlags & Components.interfaces.nsIWebProgressListener.STATE_STOP) &&
-        (aStateFlags & Components.interfaces.nsIWebProgressListener.STATE_IS_WINDOW)) {
-      let channel = aRequest.QueryInterface(Ci.nsIHttpChannel);
-      let contentType = channel.getResponseHeader("Content-Type");
-      if (contentType == "text/xml") {
-        // Stop the request so that the user doesn't see the XML, and close the
-        // content tab while we're at it.
-        this.browser.stop();
-        let tabmail = window.document.getElementById("tabmail");
-        let myTabInfo = tabmail.tabInfo
-          .filter((function (x) {
-            return "browser" in x && x.browser == this.browser;
-          }).bind(this))[0];
-        tabmail.closeTab(myTabInfo);
+httpRequestObserver.prototype = {
+  observe: function(aSubject, aTopic, aData) {
+    if (aTopic != "http-on-examine-response")
+      return;
 
-        // Fire off a request to get the XML again, this time so that we can
-        // analyze it and get its contents.
-        aRequest.QueryInterface(Ci.nsIChannel);
-        let url = aRequest.URI;
-        let newChannel = NetUtil.newChannel(url);
-        let chunks = [];
-        let self = this;
-        let inputStream = newChannel.asyncOpen({
-
-          onStartRequest: function (/* nsIRequest */ aRequest,
-                                    /* nsISupports */ aContext) {
-          },
+    if (!(aSubject instanceof Ci.nsIHttpChannel)) {
+      Component.utils.reportError("Failed to get a nsIHttpChannel when "
+                                  + "observing http-on-examine-response");
+      return;
+    }
 
-          onStopRequest: function (/* nsIRequest */ aRequest,
-                                   /* nsISupports */ aContext,
-                                   /* int */ aStatusCode) {
-            try {
-              let data = chunks.join("");
-              let xml = new XML(data);
-              let accountConfig = accountCreationFuncs.readFromXML(xml);
-              accountCreationFuncs.replaceVariables(accountConfig,
-                self.params.realName,
-                self.params.email);
-              let account = accountCreationFuncs.createAccountInBackend(accountConfig);
-              NewMailAccountProvisioner(null, {
-                success: true,
-                search_engine: self.params.searchEngine,
-                account: account,
-              });
-            } catch (e) {
-              Components.utils.reportError("Problem interpreting provider XML: "+ e);
-            }
-          },
+    let contentType = "";
+    try {
+      contentType = aSubject.getResponseHeader("Content-Type");
+    } catch(e) {
+      // If we couldn't get the response header, which can happen,
+      // just swallow the exception and return.
+      return;
+    }
 
-          onDataAvailable: function (/* nsIRequest */ aRequest,
-                                     /* nsISupports */ aContext,
-                                     /* nsIInputStream */ aStream,
-                                     /* int */ aOffset,
-                                     /* int */ aCount) {
-            let str = NetUtil.readInputStreamToString(aStream, aCount);
-            chunks.push(str);
-          },
+    if (contentType.toLowerCase().indexOf("text/xml") != 0)
+      return;
 
-          QueryInterface: XPCOMUtils.generateQI([Ci.nsIStreamListener,
-                                                 Ci.nsIRequestObserver])
+    let requestWindow = this._getWindowForRequest(aSubject);
+    if (!requestWindow || (requestWindow !== this.browser.contentWindow))
+      return;
 
-        }, null);
-      }
+    // Ok, we've got a request that looks like a decent candidate.
+    // Let's attach our TracingListener.
+    if (aSubject instanceof Ci.nsITraceableChannel) {
+      let newListener = new TracingListener(this.browser, this.params);
+      newListener.oldListener = aSubject.setNewListener(newListener);
     }
   },
 
-  onProgressChange: function (/* in nsIWebProgress */ aWebProgress,
-                              /* in nsIRequest */ aRequest,
-                              /* in long */ aCurSelfProgress,
-                              /* in long */ aMaxSelfProgress,
-                              /* in long */ aCurTotalProgress,
-                              /* in long */ aMaxTotalProgress) {
+  /**
+   * _getWindowForRequest is an internal function that takes an nsIRequest,
+   * and returns the associated window for that request.  If it cannot find
+   * an associated window, the function returns null. On exception, the
+   * exception message is logged to the Error Console and null is returned.
+   *
+   * @param aRequest the nsIRequest to analyze
+   */
+  _getWindowForRequest: function(aRequest) {
+    try {
+      if (aRequest && aRequest.notificationCallbacks) {
+        return aRequest.notificationCallbacks
+                       .getInterface(Ci.nsILoadContext)
+                       .associatedWindow;
+      }
+      if (aRequest && aRequest.loadGroup
+          && aRequest.loadGroup.notificationCallbacks) {
+        return aRequest.loadGroup
+                       .notificationCallbacks
+                       .getInterface(Ci.nsILoadContext)
+                       .associatedWindow;
+      }
+    } catch(e) {
+      Components.utils.reportError("Could not find an associated window "
+                                   + "for an HTTP request. Error: " + e);
+    }
+    return null;
   },
 
-  onLocationChange: function (/* in nsIWebProgress */ aWebProgress,
-                              /* in nsIRequest */ aRequest,
-                              /* in nsIURI */ aLocation,
-                              /* in int */ aFlags) {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
+}
+
+/**
+ * TracingListener is an nsITracableChannel implementation that copies
+ * an incoming stream of data from a request.  The data flows through this
+ * nsITracableChannel transparently to the original listener. Once the
+ * response data is fully downloaded, an attempt is made to parse it
+ * as XML, and derive email account data from it.
+ *
+ * @param aBrowser The XUL <browser> the request lives in.
+ * @param aParams An object containing various bits of information.
+ * @param aParams.realName The real name of the person
+ * @param aParams.email The email address the person picked.
+ * @param aParams.searchEngine The search engine associated to that provider.
+ */
+function TracingListener(aBrowser, aParams) {
+  this.chunks = [];
+  this.browser = aBrowser;
+  this.params = aParams;
+  this.oldListener = null;
+}
+
+TracingListener.prototype = {
+
+  onStartRequest: function (/* nsIRequest */ aRequest,
+                            /* nsISupports */ aContext) {
+    this.oldListener.onStartRequest(aRequest, aContext);
   },
 
-  onStatusChange: function (/* in nsIWebProgress */ aWebProgress,
-                            /* in nsIRequest */ aRequest,
-                            /* in nsresult */ aStatus,
-                            /* in wstring */ aMessage) {
+  onStopRequest: function (/* nsIRequest */ aRequest,
+                           /* nsISupports */ aContext,
+                           /* int */ aStatusCode) {
+    try {
+      // Attempt to construct the downloaded data into XML
+      let data = this.chunks.join("");
+      let xml = new XML(data);
+
+      // Attempt to derive email account information
+      let accountConfig = accountCreationFuncs.readFromXML(xml);
+      accountCreationFuncs.replaceVariables(accountConfig,
+        this.params.realName,
+        this.params.email);
+      let account = accountCreationFuncs.createAccountInBackend(accountConfig);
+
+      // Switch to the mail tab
+      let tabmail = document.getElementById('tabmail');
+      tabmail.switchToTab(0);
+
+      // Find the tab associated with this browser, and close it.
+      let myTabInfo = tabmail.tabInfo
+        .filter((function (x) {
+              return "browser" in x && x.browser == this.browser;
+              }).bind(this))[0];
+      tabmail.closeTab(myTabInfo);
+
+      // Respawn the account provisioner to announce our success
+      NewMailAccountProvisioner(null, {
+        success: true,
+        search_engine: this.params.searchEngine,
+        account: account,
+      });
+    } catch (e) {
+      // Something went wrong.  Right now, we just dump the problem out
+      // to the Error Console.  We should really do something smarter and
+      // more user-facing, because if - for example - a provider passes
+      // some bogus XML, this routine silently fails.
+      Components.utils.reportError("Problem interpreting provider XML:" + e);
+    }
+
+    this.oldListener.onStopRequest(aRequest, aContext, aStatusCode);
   },
 
-  onSecurityChange: function (/* in nsIWebProgress */ aWebProgress,
-                              /* in nsIRequest */ aRequest,
-                              /* in unsigned long */ aState) {
+  onDataAvailable: function (/* nsIRequest */ aRequest,
+                             /* nsISupports */ aContext,
+                             /* nsIInputStream */ aStream,
+                             /* int */ aOffset,
+                             /* int */ aCount) {
+    // We want to read the stream of incoming data, but we also want
+    // to make sure it gets passed to the original listener. We do this
+    // by passing the input stream through an nsIStorageStream, writing
+    // the data to that stream, and passing it along to the next listener.
+    let binaryInputStream = Cc["@mozilla.org/binaryinputstream;1"]
+                           .createInstance(Ci.nsIBinaryInputStream);
+    let storageStream = Cc["@mozilla.org/storagestream;1"]
+                        .createInstance(Ci.nsIStorageStream);
+    let outStream = Cc["@mozilla.org/binaryoutputstream;1"]
+                    .createInstance(Ci.nsIBinaryOutputStream);
+
+    binaryInputStream.setInputStream(aStream);
+
+    // The segment size of 8192 is a little magical - more or less
+    // copied from nsITraceableChannel example code strewn about the
+    // web.
+    storageStream.init(8192, aCount, null);
+    outStream.setOutputStream(storageStream.getOutputStream(0));
+
+    let data = binaryInputStream.readBytes(aCount);
+    this.chunks.push(data);
+
+    outStream.writeBytes(data, aCount);
+    this.oldListener.onDataAvailable(aRequest, aContext,
+                                     storageStream.newInputStream(0),
+                                     aOffset, aCount);
   },
 
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsISupportsWeakReference,
-                                         Ci.nsIWebProgressListener]),
-};
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIStreamListener,
+                                         Ci.nsIRequestObserver])
+
+}