Bug 1337259 - Don't show password autocomplete upon a right click into the password field. r=mconley draft
authorJohann Hofmann <jhofmann@mozilla.com>
Fri, 10 Feb 2017 12:45:26 +0100
changeset 488734 cbf2fe788f075e9c27b4f7ce162f08ee25dc2c5a
parent 486036 03fc0a686d3f8e0c38136771579d030c7a8c0b82
child 546804 d144a5f133aae360e750078f93662bc827a7fcfe
push id46607
push userbmo:jhofmann@mozilla.com
push dateThu, 23 Feb 2017 15:56:56 +0000
reviewersmconley
bugs1337259
milestone54.0a1
Bug 1337259 - Don't show password autocomplete upon a right click into the password field. r=mconley MozReview-Commit-ID: JEODCfZOMRW
toolkit/components/passwordmgr/test/browser/browser_context_menu_autocomplete_interaction.js
toolkit/components/satchel/nsFormFillController.cpp
toolkit/components/satchel/nsFormFillController.h
--- a/toolkit/components/passwordmgr/test/browser/browser_context_menu_autocomplete_interaction.js
+++ b/toolkit/components/passwordmgr/test/browser/browser_context_menu_autocomplete_interaction.js
@@ -2,18 +2,16 @@
  * Test the password manager context menu interaction with autocomplete.
  */
 
 "use strict";
 
 const TEST_HOSTNAME = "https://example.com";
 const BASIC_FORM_PAGE_PATH = DIRECTORY_PATH + "form_basic.html";
 
-var gUnexpectedIsTODO = false;
-
 /**
  * Initialize logins needed for the tests and disable autofill
  * for login forms for easier testing of manual fill.
  */
 add_task(function* test_initialize() {
   let autocompletePopup = document.getElementById("PopupAutoComplete");
   Services.prefs.setBoolPref("signon.autofillForms", false);
   registerCleanupFunction(() => {
@@ -35,35 +33,30 @@ add_task(function* test_context_menu_use
 
     let contextMenu = document.getElementById("contentAreaContextMenu");
     Assert.equal(contextMenu.state, "open", "Context menu opened");
     contextMenu.hidePopup();
   });
 });
 
 add_task(function* test_context_menu_password() {
-  gUnexpectedIsTODO = true;
   yield BrowserTestUtils.withNewTab({
     gBrowser,
     url: TEST_HOSTNAME + BASIC_FORM_PAGE_PATH,
   }, function* (browser) {
     yield openContextMenu(browser, "#form-basic-password");
 
     let contextMenu = document.getElementById("contentAreaContextMenu");
     Assert.equal(contextMenu.state, "open", "Context menu opened");
     contextMenu.hidePopup();
   });
 });
 
 function autocompleteUnexpectedPopupShowing(event) {
-  if (gUnexpectedIsTODO) {
-    todo(false, "Autocomplete shouldn't appear");
-  } else {
-    Assert.ok(false, "Autocomplete shouldn't appear");
-  }
+  Assert.ok(false, "Autocomplete shouldn't appear");
   event.target.hidePopup();
 }
 
 /**
  * Synthesize mouse clicks to open the context menu popup
  * for a target login input element.
  */
 function* openContextMenu(browser, loginInput) {
--- a/toolkit/components/satchel/nsFormFillController.cpp
+++ b/toolkit/components/satchel/nsFormFillController.cpp
@@ -19,34 +19,35 @@
 #include "nsIInterfaceRequestorUtils.h"
 #include "nsIDocShellTreeItem.h"
 #include "nsPIDOMWindow.h"
 #include "nsIWebNavigation.h"
 #include "nsIContentViewer.h"
 #include "nsIDOMKeyEvent.h"
 #include "nsIDOMDocument.h"
 #include "nsIDOMElement.h"
-#include "nsIFormControl.h"
 #include "nsIDocument.h"
 #include "nsIContent.h"
 #include "nsIPresShell.h"
 #include "nsRect.h"
 #include "nsIDOMHTMLFormElement.h"
 #include "nsILoginManager.h"
 #include "nsIDOMMouseEvent.h"
 #include "mozilla/ModuleUtils.h"
 #include "nsToolkitCompsCID.h"
 #include "nsEmbedCID.h"
 #include "nsIDOMNSEditableElement.h"
 #include "nsContentUtils.h"
 #include "nsILoadContext.h"
 #include "nsIFrame.h"
 #include "nsIScriptSecurityManager.h"
 #include "nsFocusManager.h"
+#include "nsThreadUtils.h"
 
+using namespace mozilla;
 using namespace mozilla::dom;
 
 NS_IMPL_CYCLE_COLLECTION(nsFormFillController,
                          mController, mLoginManager, mFocusedPopup, mDocShells,
                          mPopups, mLastListener, mLastFormAutoComplete)
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsFormFillController)
   NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIFormFillController)
@@ -62,20 +63,23 @@ NS_IMPL_CYCLE_COLLECTING_ADDREF(nsFormFi
 NS_IMPL_CYCLE_COLLECTING_RELEASE(nsFormFillController)
 
 
 
 nsFormFillController::nsFormFillController() :
   mFocusedInput(nullptr),
   mFocusedInputNode(nullptr),
   mListNode(nullptr),
+  // This matches the threshold in
+  // toolkit/components/passwordmgr/LoginManagerContent.jsm.
+  mFocusAfterContextMenuThreshold(250),
   mTimeout(50),
   mMinResultsForPopup(1),
   mMaxRows(0),
-  mContextMenuFiredBeforeFocus(false),
+  mLastContextMenuEventTimeStamp(TimeStamp::Now()),
   mDisableAutoComplete(false),
   mCompleteDefaultIndex(false),
   mCompleteSelectedIndex(false),
   mForceComplete(false),
   mSuppressOnInput(false)
 {
   mController = do_GetService("@mozilla.org/autocomplete/controller;1");
   MOZ_ASSERT(mController);
@@ -915,17 +919,19 @@ nsFormFillController::HandleEvent(nsIDOM
   }
   if (type.EqualsLiteral("compositionend")) {
     NS_ASSERTION(mController, "should have a controller!");
     if (mController && mFocusedInput)
       mController->HandleEndComposition();
     return NS_OK;
   }
   if (type.EqualsLiteral("contextmenu")) {
-    mContextMenuFiredBeforeFocus = true;
+    // Set timestamp to check for a recent contextmenu
+    // call in Focus(), to avoid showing the popup.
+    mLastContextMenuEventTimeStamp = TimeStamp::Now();
     if (mFocusedPopup)
       mFocusedPopup->ClosePopup();
     return NS_OK;
   }
   if (type.EqualsLiteral("pagehide")) {
 
     nsCOMPtr<nsIDocument> doc = do_QueryInterface(
       aEvent->InternalDOMEvent()->GetTarget());
@@ -999,42 +1005,57 @@ nsFormFillController::MaybeStartControll
     isPwmgrInput = true;
   }
 
   if (isPwmgrInput || hasList || autocomplete) {
     StartControllingInput(aInput);
   }
 }
 
+void
+nsFormFillController::FocusEventDelayedCallback(nsIFormControl* aFormControl)
+{
+  nsCOMPtr<nsIFormControl> formControl = do_QueryInterface(mFocusedInputNode);
+
+  if (!formControl || formControl != aFormControl) {
+    return;
+  }
+
+  uint64_t timeDiff = fabs((TimeStamp::Now() - mLastContextMenuEventTimeStamp).ToMilliseconds());
+  // If this focus doesn't follow a contextmenu event within our specified
+  // threshold then show the autocomplete popup for all password fields.
+  // This is done to avoid showing both the context menu and the popup
+  // at the same time. The threshold should be a low amount of time that
+  // makes it impossible for the user to accidentally trigger this condition.
+  if (timeDiff > mFocusAfterContextMenuThreshold
+      && formControl->GetType() == NS_FORM_INPUT_PASSWORD) {
+   ShowPopup();
+  }
+}
+
 nsresult
 nsFormFillController::Focus(nsIDOMEvent* aEvent)
 {
   nsCOMPtr<nsIDOMHTMLInputElement> input = do_QueryInterface(
     aEvent->InternalDOMEvent()->GetTarget());
   MaybeStartControllingInput(input);
 
   // Bail if we didn't start controlling the input.
   if (!mFocusedInputNode) {
-    mContextMenuFiredBeforeFocus = false;
     return NS_OK;
   }
 
 #ifndef ANDROID
   nsCOMPtr<nsIFormControl> formControl = do_QueryInterface(mFocusedInputNode);
   MOZ_ASSERT(formControl);
 
-  // If this focus doesn't immediately follow a contextmenu event then show
-  // the autocomplete popup for all password fields.
-  if (!mContextMenuFiredBeforeFocus
-      && formControl->GetType() == NS_FORM_INPUT_PASSWORD) {
-    ShowPopup();
-  }
+  NS_DispatchToMainThread(NewRunnableMethod<nsCOMPtr<nsIFormControl>>(
+      this, &nsFormFillController::FocusEventDelayedCallback, formControl));
 #endif
 
-  mContextMenuFiredBeforeFocus = false;
   return NS_OK;
 }
 
 nsresult
 nsFormFillController::KeyPress(nsIDOMEvent* aEvent)
 {
   NS_ASSERTION(mController, "should have a controller!");
   if (!mFocusedInput || !mController)
--- a/toolkit/components/satchel/nsFormFillController.h
+++ b/toolkit/components/satchel/nsFormFillController.h
@@ -25,16 +25,17 @@
 // X.h defines KeyPress
 #ifdef KeyPress
 #undef KeyPress
 #endif
 
 class nsFormHistory;
 class nsINode;
 class nsPIDOMWindowOuter;
+class nsIFormControl;
 
 class nsFormFillController final : public nsIFormFillController,
                                    public nsIAutoCompleteInput,
                                    public nsIAutoCompleteSearch,
                                    public nsIDOMEventListener,
                                    public nsIFormAutoCompleteObserver,
                                    public nsIMutationObserver
 {
@@ -81,16 +82,19 @@ protected:
   inline nsIDocShell *GetDocShellForInput(nsIDOMHTMLInputElement *aInput);
   inline nsPIDOMWindowOuter *GetWindowForDocShell(nsIDocShell *aDocShell);
   inline int32_t GetIndexOfDocShell(nsIDocShell *aDocShell);
 
   void MaybeRemoveMutationObserver(nsINode* aNode);
 
   void RemoveForDocument(nsIDocument* aDoc);
   bool IsEventTrusted(nsIDOMEvent *aEvent);
+
+  void FocusEventDelayedCallback(nsIFormControl* formControl);
+
   // members //////////////////////////////////////////
 
   nsCOMPtr<nsIAutoCompleteController> mController;
   nsCOMPtr<nsILoginManager> mLoginManager;
   nsIDOMHTMLInputElement* mFocusedInput;
   nsINode* mFocusedInputNode;
 
   // mListNode is a <datalist> element which, is set, has the form fill controller
@@ -107,20 +111,21 @@ protected:
 
   // This is cleared by StopSearch().
   nsCOMPtr<nsIFormAutoComplete> mLastFormAutoComplete;
   nsString mLastSearchString;
 
   nsDataHashtable<nsPtrHashKey<const nsINode>, bool> mPwmgrInputs;
   nsDataHashtable<nsPtrHashKey<const nsINode>, bool> mAutofillInputs;
 
+  uint16_t mFocusAfterContextMenuThreshold;
   uint32_t mTimeout;
   uint32_t mMinResultsForPopup;
   uint32_t mMaxRows;
-  bool mContextMenuFiredBeforeFocus;
+  mozilla::TimeStamp mLastContextMenuEventTimeStamp;
   bool mDisableAutoComplete;
   bool mCompleteDefaultIndex;
   bool mCompleteSelectedIndex;
   bool mForceComplete;
   bool mSuppressOnInput;
 };
 
 #endif // __nsFormFillController__