Bug 1176399 - Multiple requests for master password when GMail OAuth2 is enabled. r=mkmelin a=jorgk DONTBUILD BETA_BASE_20170418
authorPhilipp Kewisch <mozilla@kewis.ch>
Thu, 24 Nov 2016 02:07:21 +0100
changeset 24474 05d563fd17e9
parent 24473 f3570179a368
child 24475 4baa39ba286e
push id2061
push usermozilla@jorgk.com
push dateTue, 18 Apr 2017 13:15:21 +0000
treeherdercomm-aurora@05d563fd17e9 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmkmelin, jorgk
bugs1176399
Bug 1176399 - Multiple requests for master password when GMail OAuth2 is enabled. r=mkmelin a=jorgk DONTBUILD MozReview-Commit-ID: KP0v3zGhTT7
mail/components/im/modules/chatHandler.jsm
mailnews/base/public/nsIMsgAsyncPrompter.idl
mailnews/base/src/msgAsyncPrompter.js
mailnews/base/src/msgOAuth2Module.js
mailnews/imap/src/nsImapProtocol.cpp
mailnews/local/src/nsPop3Protocol.cpp
mailnews/news/src/nsNNTPProtocol.cpp
--- a/mail/components/im/modules/chatHandler.jsm
+++ b/mail/components/im/modules/chatHandler.jsm
@@ -28,16 +28,19 @@ var ChatCore = {
     Services.obs.addObserver(this, "contact-removed", false);
 
     // The initialization of the im core may trigger a master password prompt,
     // so wrap it with the async prompter service. Note this service already
     // waits for the asynchronous initialization of the password service.
     Components.classes["@mozilla.org/messenger/msgAsyncPrompter;1"]
               .getService(Components.interfaces.nsIMsgAsyncPrompter)
               .queueAsyncAuthPrompt("im", false, {
+      onPromptStartAsync: function(callback) {
+        callback.onAuthResult(this.onPromptStart());
+      },
       onPromptStart: function() {
         Services.core.init();
 
         // Find the accounts that exist in the im account service but
         // not in nsMsgAccountManager. They have probably been lost if
         // the user has used an older version of Thunderbird on a
         // profile with IM accounts. See bug 736035.
         let accountsById = {};
--- a/mailnews/base/public/nsIMsgAsyncPrompter.idl
+++ b/mailnews/base/public/nsIMsgAsyncPrompter.idl
@@ -30,31 +30,47 @@ interface nsIMsgAsyncPrompter : nsISuppo
    *                           immediately may not be synchronously, on OS/X.
    * @param aCaller An nsIMsgAsyncPromptListener to call back to when the prompt
    *                is ready to be made.
    */
   void queueAsyncAuthPrompt(in ACString aKey, in boolean aPromptImmediately,
                             in nsIMsgAsyncPromptListener aCaller);
 };
 
+[scriptable, function, uuid(acca94c9-378e-46e3-9a91-6655bf9c91a3)]
+interface nsIMsgAsyncPromptCallback : nsISupports {
+  /**
+   * Called when an auth result is available. Can be passed as a function.
+   *
+   * @param aResult   True if there is auth information available following the
+   *                    prompt, false otherwise.
+   */
+  void onAuthResult(in boolean aResult);
+};
+
 /**
  * This is used in combination with nsIMsgAsyncPrompter.
  */
 [scriptable, uuid(fb5307a3-39d0-462e-92c8-c5c288a2612f)]
 interface nsIMsgAsyncPromptListener : nsISupports {
   /**
-   * Called when the listener should do its prompt. The listener
-   * should not return until the prompt is complete.
-   *
-   * @return  True if there is auth information available following the prompt,
-   *          false otherwise.
+   * This method has been deprecated, please use onPromptStartAsync instead.
    */
   boolean onPromptStart();
 
   /**
+   * Called when the listener should do its prompt. This can happen
+   * synchronously or asynchronously, but in any case when done the callback
+   * method should be called.
+   *
+   * @param aCallback   The callback to execute when auth prompt has completed.
+   */
+  void onPromptStartAsync(in nsIMsgAsyncPromptCallback aCallback);
+
+  /**
    * Called in the case that the queued prompt was combined with another and
    * there is now authentication information available.
    */
   void onPromptAuthAvailable();
 
   /**
    * Called in the case that the queued prompt was combined with another but
    * the prompt was canceled.
--- a/mailnews/base/src/msgAsyncPrompter.js
+++ b/mailnews/base/src/msgAsyncPrompter.js
@@ -1,12 +1,13 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * 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/. */
 
+Components.utils.import("resource://gre/modules/Deprecated.jsm");
 Components.utils.import("resource://gre/modules/Services.jsm");
 Components.utils.import("resource://gre/modules/Task.jsm");
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 Components.utils.import("resource:///modules/gloda/log4moz.js");
 
 var Ci = Components.interfaces;
 var Cc = Components.classes;
 
@@ -14,38 +15,56 @@ function runnablePrompter(asyncPrompter,
   this._asyncPrompter = asyncPrompter;
   this._hashKey = hashKey;
 }
 
 runnablePrompter.prototype = {
   _asyncPrompter: null,
   _hashKey: null,
 
+  _promiseAuthPrompt: function(listener) {
+    return new Promise((resolve, reject) => {
+      try {
+        listener.onPromptStartAsync({ onAuthResult: resolve });
+      } catch (e) {
+        if (e.result == Components.results.NS_ERROR_XPC_JSOBJECT_HAS_NO_FUNCTION_NAMED) {
+          // Fall back to onPromptStart, for add-ons compat
+          Deprecated.warning("onPromptStart has been replaced by onPromptStartAsync",
+                             "https://bugzilla.mozilla.org/show_bug.cgi?id=1176399");
+          let ok = listener.onPromptStart();
+          resolve(ok);
+        } else {
+          reject(e);
+        }
+      }
+    });
+  },
+
   run: Task.async(function *() {
     yield Services.logins.initializationPromise;
     this._asyncPrompter._log.debug("Running prompt for " + this._hashKey);
     let prompter = this._asyncPrompter._pendingPrompts[this._hashKey];
     let ok = false;
     try {
-      ok = prompter.first.onPromptStart();
-    }
-    catch (ex) {
+      ok = yield this._promiseAuthPrompt(prompter.first);
+    } catch (ex) {
       Components.utils.reportError("runnablePrompter:run: " + ex + "\n");
+      prompter.first.onPromptCanceled();
     }
 
     delete this._asyncPrompter._pendingPrompts[this._hashKey];
 
     for (var consumer of prompter.consumers) {
       try {
-        if (ok)
+        if (ok) {
           consumer.onPromptAuthAvailable();
-        else
+        } else {
           consumer.onPromptCanceled();
-      }
-      catch (ex) {
+        }
+      } catch (ex) {
         // Log the error for extension devs and others to pick up.
         Components.utils.reportError("runnablePrompter:run: consumer.onPrompt* reported an exception: " + ex + "\n");
       }
     }
     this._asyncPrompter._asyncPromptInProgress--;
 
     this._asyncPrompter._log.debug("Finished running prompter for " + this._hashKey);
     this._asyncPrompter._doAsyncAuthPrompt();
--- a/mailnews/base/src/msgOAuth2Module.js
+++ b/mailnews/base/src/msgOAuth2Module.js
@@ -121,29 +121,53 @@ OAuth2Module.prototype = {
           loginMgr.modifyLogin(login, propBag);
         }
         else
           loginMgr.removeLogin(login);
         return token;
       }
     }
 
-    // Otherwise, we need a new login, so create one and fill it in.
-    let login = Cc["@mozilla.org/login-manager/loginInfo;1"]
-                  .createInstance(Ci.nsILoginInfo);
-    login.init(this._loginUrl, null, this._scope, this._username, token,
-      '', '');
-    loginMgr.addLogin(login);
+    // Unless the token is null, we need to create and fill in a new login
+    if (token) {
+      let login = Cc["@mozilla.org/login-manager/loginInfo;1"]
+                    .createInstance(Ci.nsILoginInfo);
+      login.init(this._loginUrl, null, this._scope, this._username, token,
+        '', '');
+      loginMgr.addLogin(login);
+    }
     return token;
   },
 
   connect(aWithUI, aListener) {
-    this._oauth.connect(() => aListener.onSuccess(this._oauth.accessToken),
-                        x => aListener.onFailure(x),
-                        aWithUI, false);
+    let oauth = this._oauth;
+    let promptlistener = {
+      onPromptStartAsync: function(callback) {
+        oauth.connect(() => {
+          this.onPromptAuthAvailable();
+          callback.onAuthResult(true);
+        }, (err) => {
+          this.onPromptCanceled();
+          callback.onAuthResult(false);
+        }, aWithUI, false);
+      },
+
+      onPromptAuthAvailable: function() {
+        aListener.onSuccess(oauth.accessToken);
+      },
+      onPromptCanceled: function() {
+        aListener.onFailure(Components.results.NS_ERROR_ABORT);
+      },
+      onPromptStart: function() {}
+    };
+
+    let asyncprompter = Components.classes["@mozilla.org/messenger/msgAsyncPrompter;1"]
+                                  .getService(Components.interfaces.nsIMsgAsyncPrompter);
+    let promptkey = this._loginUrl + "/" + this._username;
+    asyncprompter.queueAsyncAuthPrompt(promptkey, false, promptlistener);
   },
 
   buildXOAuth2String() {
     return btoa("user=" + this._username + "\x01auth=Bearer " +
       this._oauth.accessToken + "\x01\x01");
   },
 };
 
--- a/mailnews/imap/src/nsImapProtocol.cpp
+++ b/mailnews/imap/src/nsImapProtocol.cpp
@@ -8361,16 +8361,23 @@ nsresult nsImapProtocol::GetPassword(nsC
       password = m_password;
     }
   }
   if (!password.IsEmpty())
     m_lastPasswordSent = password;
   return rv;
 }
 
+NS_IMETHODIMP nsImapProtocol::OnPromptStartAsync(nsIMsgAsyncPromptCallback *aCallback)
+{
+  bool result = false;
+  OnPromptStart(&result);
+  return aCallback->OnAuthResult(result);
+}
+
 // This is called from the UI thread.
 NS_IMETHODIMP
 nsImapProtocol::OnPromptStart(bool *aResult)
 {
   nsresult rv;
   nsCOMPtr<nsIImapIncomingServer> imapServer = do_QueryReferent(m_server, &rv);
   NS_ENSURE_SUCCESS(rv, rv);
   nsCOMPtr<nsIMsgWindow> msgWindow;
--- a/mailnews/local/src/nsPop3Protocol.cpp
+++ b/mailnews/local/src/nsPop3Protocol.cpp
@@ -788,16 +788,23 @@ nsresult nsPop3Protocol::StartGetAsyncPa
 
   rv = asyncPrompter->QueueAsyncAuthPrompt(server, false, this);
   // Explict NS_ENSURE_SUCCESS for debug purposes as errors tend to get
   // hidden.
   NS_ENSURE_SUCCESS(rv, rv);
   return rv;
 }
 
+NS_IMETHODIMP nsPop3Protocol::OnPromptStartAsync(nsIMsgAsyncPromptCallback *aCallback)
+{
+  bool result = false;
+  OnPromptStart(&result);
+  return aCallback->OnAuthResult(result);
+}
+
 NS_IMETHODIMP nsPop3Protocol::OnPromptStart(bool *aResult)
 {
   MOZ_LOG(POP3LOGMODULE, LogLevel::Debug, (POP3LOG("OnPromptStart()")));
 
   *aResult = false;
 
   nsresult rv;
   nsCOMPtr<nsIMsgIncomingServer> server = do_QueryInterface(m_pop3Server, &rv);
--- a/mailnews/news/src/nsNNTPProtocol.cpp
+++ b/mailnews/news/src/nsNNTPProtocol.cpp
@@ -2515,16 +2515,23 @@ nsresult nsNNTPProtocol::PasswordRespons
     HandleAuthenticationFailure();
     return NS_OK;
   }
 
   NS_ERROR("should never get here");
   return NS_ERROR_FAILURE;
 }
 
+NS_IMETHODIMP nsNNTPProtocol::OnPromptStartAsync(nsIMsgAsyncPromptCallback *aCallback)
+{
+  bool result = false;
+  OnPromptStart(&result);
+  return aCallback->OnAuthResult(result);
+}
+
 NS_IMETHODIMP nsNNTPProtocol::OnPromptStart(bool *authAvailable)
 {
   NS_ENSURE_ARG_POINTER(authAvailable);
   NS_ENSURE_STATE(m_nextState == NNTP_SUSPENDED);
   
   if (!m_newsFolder)
   {
     // If we don't have a news folder, we may have been closed already.