Bug 1379466 - Add override pref to restore legacy non-primary click dispatch on specific domains. r=smaug
authorIan Moody <moz-ian@perix.co.uk>
Thu, 18 Apr 2019 12:57:37 +0000
changeset 470163 23ff9cd1a1a43d794dad766662530471b31a121f
parent 470162 0e9fa06f3fd885525adaad2e9995e3077258c94c
child 470164 f1e0b621963c9ecae6e9b070da972d3ecd970c68
push id112843
push useraiakab@mozilla.com
push dateFri, 19 Apr 2019 09:50:22 +0000
treeherdermozilla-inbound@c06f27cbfe40 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs1379466
milestone68.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 1379466 - Add override pref to restore legacy non-primary click dispatch on specific domains. r=smaug If needed for web-compat. Also stop dispatching auxclicks if non-primary click has been preventDefaulted, so that legacy new-tab prevention can work with the pref flip. Differential Revision: https://phabricator.services.mozilla.com/D27364
dom/events/EventStateManager.cpp
dom/events/test/mochitest.ini
dom/events/test/test_legacy_non-primary_click.html
layout/base/PresShell.cpp
layout/base/PresShell.h
modules/libpref/init/all.js
widget/MouseEvents.h
--- a/dom/events/EventStateManager.cpp
+++ b/dom/events/EventStateManager.cpp
@@ -4858,17 +4858,18 @@ nsresult EventStateManager::InitAndDispa
                          aMouseUpEvent->mWidget, WidgetMouseEvent::eReal);
 
   event.mRefPoint = aMouseUpEvent->mRefPoint;
   event.mClickCount = aMouseUpEvent->mClickCount;
   event.mModifiers = aMouseUpEvent->mModifiers;
   event.buttons = aMouseUpEvent->buttons;
   event.mTime = aMouseUpEvent->mTime;
   event.mTimeStamp = aMouseUpEvent->mTimeStamp;
-  event.mFlags.mOnlyChromeDispatch = aNoContentDispatch;
+  event.mFlags.mOnlyChromeDispatch =
+      aNoContentDispatch && !aMouseUpEvent->mUseLegacyNonPrimaryDispatch;
   event.mFlags.mNoContentDispatch = aNoContentDispatch;
   event.button = aMouseUpEvent->button;
   event.pointerId = aMouseUpEvent->pointerId;
   event.inputSource = aMouseUpEvent->inputSource;
   nsIContent* target = aMouseUpContent;
   nsIFrame* targetFrame = aCurrentTarget;
   if (aOverrideClickTarget) {
     target = aOverrideClickTarget;
@@ -4987,17 +4988,18 @@ nsresult EventStateManager::DispatchClic
   nsresult rv = InitAndDispatchClickEvent(
       aMouseUpEvent, aStatus, eMouseClick, aPresShell, aClickTarget,
       currentTarget, notDispatchToContents, aOverrideClickTarget);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   // Fire auxclick event if necessary.
-  if (fireAuxClick && aClickTarget && aClickTarget->IsInComposedDoc()) {
+  if (fireAuxClick && *aStatus != nsEventStatus_eConsumeNoDefault &&
+      aClickTarget && aClickTarget->IsInComposedDoc()) {
     rv = InitAndDispatchClickEvent(aMouseUpEvent, aStatus, eMouseAuxClick,
                                    aPresShell, aClickTarget, currentTarget,
                                    false, aOverrideClickTarget);
     NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to dispatch eMouseAuxClick");
   }
 
   // Fire double click event if click count is 2.
   if (aMouseUpEvent->mClickCount == 2 && !fireAuxClick && aClickTarget &&
--- a/dom/events/test/mochitest.ini
+++ b/dom/events/test/mochitest.ini
@@ -174,16 +174,17 @@ skip-if = toolkit == 'android' #TIMED_OU
 skip-if = toolkit == 'android' #CRASH_DUMP, RANDOM
 [test_eventctors_sensors.html]
 [test_disabled_events.html]
 [test_eventhandler_scoping.html]
 [test_eventTimeStamp.html]
 [test_focus_disabled.html]
 [test_focus_abspos.html]
 [test_legacy_event.html]
+[test_legacy_non-primary_click.html]
 [test_legacy_touch_api.html]
 [test_messageEvent.html]
 [test_messageEvent_init.html]
 [test_moz_mouse_pixel_scroll_event.html]
 [test_offsetxy.html]
 [test_onerror_handler_args.html]
 [test_passive_listeners.html]
 [test_paste_image.html]
new file mode 100644
--- /dev/null
+++ b/dom/events/test/test_legacy_non-primary_click.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Test for dispatching of legacy non-primary click when domain in pref</title>
+  <script src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script src="/tests/SimpleTest/EventUtils.js"></script>
+  <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<a id="link-test" href="example.org">example link</a>
+<script>
+"use strict";
+SimpleTest.waitForExplicitFinish();
+
+const HACK_PREF = "dom.mouseevent.click.hack.use_legacy_non-primary_dispatch";
+const testEl = document.getElementById("test");
+const linkEl = document.getElementById("link-test");
+let seenClick = false;
+
+SpecialPowers.pushPrefEnv(
+  { set: [[HACK_PREF, document.domain]] },
+  SimpleTest.waitForFocus(() => {
+    // Test seeing the non-primary 'click'
+    document.addEventListener("click", (e) => {
+      ok(true, "Saw 'click' event");
+      seenClick = true;
+    }, { once: true });
+    document.addEventListener("auxclick", (e) => {
+      ok(true, "Saw 'auxclick' event");
+      ok(seenClick, "Saw 'click' event before 'auxclick' event");
+    }, { once: true });
+    synthesizeMouseAtCenter(testEl, { button: 1 });
+
+    // Test preventDefaulting on non-primary 'click'
+    document.addEventListener("click", (e) => {
+      is(e.target, linkEl, "Saw 'click' on link");
+      e.preventDefault();
+      SimpleTest.finish();
+    }, { once: true, capture: true });
+    document.addEventListener("auxclick", (e) => {
+      ok(false, "Shouldn't have got 'auxclick' after preventDefaulting 'click'");
+    }, { once: true });
+    synthesizeMouseAtCenter(linkEl, { button: 1 });
+  })
+);
+</script>
+</body>
+</html>
--- a/layout/base/PresShell.cpp
+++ b/layout/base/PresShell.cpp
@@ -832,17 +832,19 @@ PresShell::PresShell()
       mNextPaintCompressed(false),
       mHasCSSBackgroundColor(false),
       mIsLastChromeOnlyEscapeKeyConsumed(false),
       mHasReceivedPaintMessage(false),
       mIsLastKeyDownCanceled(false),
       mHasHandledUserInput(false),
       mForceDispatchKeyPressEventsForNonPrintableKeys(false),
       mForceUseLegacyKeyCodeAndCharCodeValues(false),
-      mInitializedWithKeyPressEventDispatchingBlacklist(false) {
+      mInitializedWithKeyPressEventDispatchingBlacklist(false),
+      mForceUseLegacyNonPrimaryDispatch(false),
+      mInitializedWithClickEventDispatchingBlacklist(false) {
   MOZ_LOG(gLog, LogLevel::Debug, ("PresShell::PresShell this=%p", this));
 
 #ifdef MOZ_REFLOW_PERF
   mReflowCountMgr = MakeUnique<ReflowCountMgr>();
   mReflowCountMgr->SetPresContext(mPresContext);
   mReflowCountMgr->SetPresShell(this);
 #endif
   mLastOSWake = mLoadBegin = TimeStamp::Now();
@@ -8179,16 +8181,36 @@ nsresult PresShell::EventHandler::Dispat
                                             "use_legacy_keycode_and_charcode");
       }
       if (mPresShell->mForceDispatchKeyPressEventsForNonPrintableKeys) {
         aEvent->mFlags.mOnlySystemGroupDispatchInContent = false;
       }
       if (mPresShell->mForceUseLegacyKeyCodeAndCharCodeValues) {
         aEvent->AsKeyboardEvent()->mUseLegacyKeyCodeAndCharCodeValues = true;
       }
+    } else if (aEvent->mMessage == eMouseUp) {
+      // Historically Firefox has dispatched click events for non-primary
+      // buttons, but only on window and document (and inside input/textarea),
+      // not on elements in general. The UI events spec forbids click (and
+      // dblclick) for non-primary mouse buttons, and specifies auxclick
+      // instead. In case of some websites that rely on non-primary click to
+      // prevent new tab etc. and don't have auxclick code to do the same, we
+      // need to revert to the historial non-standard behaviour
+      if (!mPresShell->mInitializedWithClickEventDispatchingBlacklist) {
+        mPresShell->mInitializedWithClickEventDispatchingBlacklist = true;
+        nsCOMPtr<nsIURI> uri =
+            GetDocumentURIToCompareWithBlacklist(*mPresShell);
+        mPresShell->mForceUseLegacyNonPrimaryDispatch =
+            nsContentUtils::IsURIInPrefList(
+                uri,
+                "dom.mouseevent.click.hack.use_legacy_non-primary_dispatch");
+      }
+      if (mPresShell->mForceUseLegacyNonPrimaryDispatch) {
+        aEvent->AsMouseEvent()->mUseLegacyNonPrimaryDispatch = true;
+      }
     }
 
     if (aEvent->mClass == eCompositionEventClass) {
       IMEStateManager::DispatchCompositionEvent(
           eventTarget, GetPresContext(), TabParent::GetFocused(),
           aEvent->AsCompositionEvent(), aEventStatus, eventCBPtr);
     } else {
       EventDispatcher::Dispatch(eventTarget, GetPresContext(), aEvent, nullptr,
--- a/layout/base/PresShell.h
+++ b/layout/base/PresShell.h
@@ -1424,16 +1424,21 @@ class PresShell final : public nsIPresSh
   // value is zero to the other value or not.  When this is set to true, we
   // should keep using legacy keyCode and charCode values (i.e., one of them
   // is always 0).
   bool mForceUseLegacyKeyCodeAndCharCodeValues : 1;
   // Whether mForceDispatchKeyPressEventsForNonPrintableKeys and
   // mForceUseLegacyKeyCodeAndCharCodeValues are initialized.
   bool mInitializedWithKeyPressEventDispatchingBlacklist : 1;
 
+  // Whether we should dispatch click events for non-primary mouse buttons.
+  bool mForceUseLegacyNonPrimaryDispatch : 1;
+  // Whether mForceUseLegacyNonPrimaryDispatch is initialised.
+  bool mInitializedWithClickEventDispatchingBlacklist : 1;
+
   static bool sDisableNonTestMouseEvents;
 
   TimeStamp mLastOSWake;
 
   static bool sProcessInteractable;
 };
 
 }  // namespace mozilla
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -224,16 +224,22 @@ pref("dom.keyboardevent.keypress.hack.di
 
 // Blacklist of domains of web apps which handle keyCode and charCode of
 // keypress events with a path only for Firefox (i.e., broken if we set
 // non-zero keyCode or charCode value to the other).  The format is exactly
 // same as "dom.keyboardevent.keypress.hack.dispatch_non_printable_keys". So,
 // check its explanation for the detail.
 pref("dom.keyboardevent.keypress.hack.use_legacy_keycode_and_charcode", "*.collabserv.com,*.gov.online.office365.us,*.officeapps-df.live.com,*.officeapps.live.com,*.online.office.de,*.partner.officewebapps.cn,*.scniris.com");
 
+// Blacklist of domains of web apps which listen for non-primary click events
+// on window global or document. The format is exactly same as
+// "dom.keyboardevent.keypress.hack.dispatch_non_printable_keys". So, check its
+// explanation for the detail.
+pref("dom.mouseevent.click.hack.use_legacy_non-primary_dispatch", "");
+
 // Whether InputEvent.data is enabled.
 pref("dom.inputevent.data.enabled", true);
 
 // Whether InputEvent.dataTransfer is enabled.
 pref("dom.inputevent.datatransfer.enabled", true);
 
 // Whether InputEvent.inputType is enabled.
 pref("dom.inputevent.inputtype.enabled", true);
--- a/widget/MouseEvents.h
+++ b/widget/MouseEvents.h
@@ -207,39 +207,42 @@ class WidgetMouseEvent : public WidgetMo
   enum ExitFrom : ExitFromType { eChild, eTopLevel };
 
  protected:
   WidgetMouseEvent()
       : mReason(eReal),
         mContextMenuTrigger(eNormal),
         mExitFrom(eChild),
         mIgnoreRootScrollFrame(false),
-        mClickCount(0) {}
+        mClickCount(0),
+        mUseLegacyNonPrimaryDispatch(false) {}
 
   WidgetMouseEvent(bool aIsTrusted, EventMessage aMessage, nsIWidget* aWidget,
                    EventClassID aEventClassID, Reason aReason)
       : WidgetMouseEventBase(aIsTrusted, aMessage, aWidget, aEventClassID),
         mReason(aReason),
         mContextMenuTrigger(eNormal),
         mExitFrom(eChild),
         mIgnoreRootScrollFrame(false),
-        mClickCount(0) {}
+        mClickCount(0),
+        mUseLegacyNonPrimaryDispatch(false) {}
 
  public:
   virtual WidgetMouseEvent* AsMouseEvent() override { return this; }
 
   WidgetMouseEvent(bool aIsTrusted, EventMessage aMessage, nsIWidget* aWidget,
                    Reason aReason,
                    ContextMenuTrigger aContextMenuTrigger = eNormal)
       : WidgetMouseEventBase(aIsTrusted, aMessage, aWidget, eMouseEventClass),
         mReason(aReason),
         mContextMenuTrigger(aContextMenuTrigger),
         mExitFrom(eChild),
         mIgnoreRootScrollFrame(false),
-        mClickCount(0) {
+        mClickCount(0),
+        mUseLegacyNonPrimaryDispatch(false) {
     if (aMessage == eContextMenu) {
       button = (mContextMenuTrigger == eNormal) ? eRightButton : eLeftButton;
     }
   }
 
 #ifdef DEBUG
   virtual ~WidgetMouseEvent() {
     NS_WARNING_ASSERTION(
@@ -284,22 +287,27 @@ class WidgetMouseEvent : public WidgetMo
   // Whether the event should ignore scroll frame bounds during dispatch.
   bool mIgnoreRootScrollFrame;
 
   // mClickCount may be non-zero value when mMessage is eMouseDown, eMouseUp,
   // eMouseClick or eMouseDoubleClick. The number is count of mouse clicks.
   // Otherwise, this must be 0.
   uint32_t mClickCount;
 
+  // Indicates whether the event should dispatch click events for non-primary
+  // mouse buttons on window and document.
+  bool mUseLegacyNonPrimaryDispatch;
+
   void AssignMouseEventData(const WidgetMouseEvent& aEvent, bool aCopyTargets) {
     AssignMouseEventBaseData(aEvent, aCopyTargets);
     AssignPointerHelperData(aEvent, /* aCopyCoalescedEvents */ true);
 
     mIgnoreRootScrollFrame = aEvent.mIgnoreRootScrollFrame;
     mClickCount = aEvent.mClickCount;
+    mUseLegacyNonPrimaryDispatch = aEvent.mUseLegacyNonPrimaryDispatch;
   }
 
   /**
    * Returns true if the event is a context menu event caused by key.
    */
   bool IsContextMenuKeyEvent() const {
     return mMessage == eContextMenu && mContextMenuTrigger == eContextMenuKey;
   }