Bug 1521396 - Make ClickHandlerChild prevent multiple action of middle click on link element for preventing middle click paste r=smaug
authorMasayuki Nakano <masayuki@d-toybox.com>
Tue, 22 Jan 2019 07:28:55 +0000
changeset 514799 81c98263341cb237c3569dae658ef03e68bf4bad
parent 514798 f1ed43b739595252601d54c07f0b8ac838351d73
child 514800 c7fa6df4337cc9e42e1acfdeb25d313fc8d624a8
push id1953
push userffxbld-merge
push dateMon, 11 Mar 2019 12:10:20 +0000
treeherdermozilla-release@9c35dcbaa899 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs1521396
milestone66.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 1521396 - Make ClickHandlerChild prevent multiple action of middle click on link element for preventing middle click paste r=smaug When user middle clicks a link, most users must not expect to expose clipboard content to the web application. Therefore, we should stop firing paste event when user click a link with middle button. This patch makes ClickHandlerChild.handleEvent() prevent multiple action when it posts middle click event on a link. Note that even if middle click event is consumed, default event handler will dispatch paste event. Unfortunately, this is compatible behavior with the other browsers. Therefore, we cannot change this behavior with calling preventDefault() and this is the reason why this patch adds Event.preventMultipleActions(). Out of scope of this bug though, if there is an element which looks like a link but implemented with JS, web apps can steal clipboard content if user enables middle click event and user just wants to open the link in new tab. It might be better to stop dispatching paste event in any browsers and request to change each web apps. Differential Revision: https://phabricator.services.mozilla.com/D17209
browser/actors/ClickHandlerChild.jsm
browser/base/content/test/tabs/browser.ini
browser/base/content/test/tabs/browser_paste_event_at_middle_click_on_link.js
browser/base/content/test/tabs/file_anchor_elements.html
dom/events/Event.h
dom/webidl/Event.webidl
--- a/browser/actors/ClickHandlerChild.jsm
+++ b/browser/actors/ClickHandlerChild.jsm
@@ -83,16 +83,26 @@ class ClickHandlerChild extends ActorChi
           let isPrivateWin = ownerDoc.nodePrincipal.originAttributes.privateBrowsingId > 0;
           sm.checkSameOriginURI(docshell.mixedContentChannel.URI, targetURI, false, isPrivateWin);
           json.allowMixedContent = true;
         } catch (e) {}
       }
       json.originPrincipal = ownerDoc.nodePrincipal;
       json.triggeringPrincipal = ownerDoc.nodePrincipal;
 
+      // If a link element is clicked with middle button, user wants to open
+      // the link somewhere rather than pasting clipboard content.  Therefore,
+      // when it's clicked with middle button, we should prevent multiple
+      // actions here to avoid leaking clipboard content unexpectedly.
+      // Note that whether the link will work actually or not does not matter
+      // because in this case, user does not intent to paste clipboard content.
+      if (event.button === 1) {
+        event.preventMultipleActions();
+      }
+
       this.mm.sendAsyncMessage("Content:Click", json);
       return;
     }
 
     // This might be middle mouse navigation.
     if (event.button == 1) {
       this.mm.sendAsyncMessage("Content:Click", json);
     }
--- a/browser/base/content/test/tabs/browser.ini
+++ b/browser/base/content/test/tabs/browser.ini
@@ -55,16 +55,19 @@ support-files = file_new_tab_page.html
 [browser_new_tab_in_privileged_process_pref.js]
 skip-if = !e10s # Pref and test only relevant for e10s.
 [browser_new_web_tab_in_file_process_pref.js]
 skip-if = !e10s # Pref and test only relevant for e10s.
 [browser_newwindow_tabstrip_overflow.js]
 [browser_open_newtab_start_observer_notification.js]
 [browser_opened_file_tab_navigated_to_web.js]
 [browser_overflowScroll.js]
+[browser_paste_event_at_middle_click_on_link.js]
+subsuite = clipboard
+support-files = file_anchor_elements.html
 [browser_pinnedTabs_clickOpen.js]
 [browser_pinnedTabs_closeByKeyboard.js]
 [browser_pinnedTabs.js]
 [browser_positional_attributes.js]
 skip-if = (verify && (os == 'win' || os == 'mac'))
 [browser_preloadedBrowser_zoom.js]
 [browser_reload_deleted_file.js]
 skip-if = (debug && os == 'mac') || (debug && os == 'linux' && bits == 64) #Bug 1421183, disabled on Linux/OSX for leaked windows
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_paste_event_at_middle_click_on_link.js
@@ -0,0 +1,74 @@
+"use strict";
+
+add_task(async function doCheckPasteEventAtMiddleClickOnAnchorElement() {
+  await SpecialPowers.pushPrefEnv({set: [
+    ["browser.tabs.opentabfor.middleclick", true],
+    ["middlemouse.paste", true],
+    ["middlemouse.contentLoadURL", false],
+    ["general.autoScroll", false],
+  ]});
+
+  await new Promise((resolve, reject) => {
+    SimpleTest.waitForClipboard("Text in the clipboard", () => {
+      Cc["@mozilla.org/widget/clipboardhelper;1"]
+        .getService(Ci.nsIClipboardHelper)
+        .copyString("Text in the clipboard");
+    }, resolve, () => {
+      ok(false, "Clipboard copy failed");
+      reject();
+    });
+  });
+
+  is(gBrowser.tabs.length, 1, "Number of tabs should be 1 at starting this test #1");
+
+  let pageURL = getRootDirectory(gTestPath).replace("chrome://mochitests/content", "http://example.com");
+  pageURL = `${pageURL}file_anchor_elements.html`;
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageURL);
+
+  let pasteEventCount = 0;
+  BrowserTestUtils.addContentEventListener(gBrowser.selectedBrowser, "paste", () => { ++pasteEventCount; });
+
+  // Click the usual link.
+  ok(true, "Clicking on usual link...");
+  let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, "http://example.com/#a_with_href", true);
+  await BrowserTestUtils.synthesizeMouseAtCenter("#a_with_href",
+                                                 {button: 1}, gBrowser.selectedBrowser);
+  let openTabForUsualLink = await newTabPromise;
+  is(openTabForUsualLink.linkedBrowser.currentURI.spec, "http://example.com/#a_with_href",
+     "Middle click should open site to correct url at clicking on usual link");
+  is(pasteEventCount, 0, "paste event should be suppressed when clicking on usual link");
+
+  // Click the link in editing host.
+  is(gBrowser.tabs.length, 3, "Number of tabs should be 3 at starting this test #2");
+  ok(true, "Clicking on editable link...");
+  await BrowserTestUtils.synthesizeMouseAtCenter("#editable_a_with_href",
+                                                 {button: 1}, gBrowser.selectedBrowser);
+  await TestUtils.waitForCondition(() => pasteEventCount >= 1,
+                                   "Waiting for paste event caused by clicking on editable link");
+  is(pasteEventCount, 1, "paste event should be suppressed when clicking on editable link");
+  is(gBrowser.tabs.length, 3, "Clicking on editable link shouldn't open new tab");
+
+  // Click the link in non-editable area in editing host.
+  ok(true, "Clicking on non-editable link in an editing host...");
+  newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, "http://example.com/#non-editable_a_with_href", true);
+  await BrowserTestUtils.synthesizeMouseAtCenter("#non-editable_a_with_href",
+                                                 {button: 1}, gBrowser.selectedBrowser);
+  let openTabForNonEditableLink = await newTabPromise;
+  is(openTabForNonEditableLink.linkedBrowser.currentURI.spec, "http://example.com/#non-editable_a_with_href",
+     "Middle click should open site to correct url at clicking on non-editable link in an editing host.");
+  is(pasteEventCount, 1, "paste event should be suppressed when clicking on non-editable link in an editing host");
+
+  // Click the <a> element without href attribute.
+  is(gBrowser.tabs.length, 4, "Number of tabs should be 4 at starting this test #3");
+  ok(true, "Clicking on anchor element without href...");
+  await BrowserTestUtils.synthesizeMouseAtCenter("#a_with_name",
+                                                 {button: 1}, gBrowser.selectedBrowser);
+  await TestUtils.waitForCondition(() => pasteEventCount >= 2,
+                                   "Waiting for paste event caused by clicking on anchor element without href");
+  is(pasteEventCount, 2, "paste event should be suppressed when clicking on anchor element without href");
+  is(gBrowser.tabs.length, 4, "Clicking on anchor element without href shouldn't open new tab");
+
+  BrowserTestUtils.removeTab(tab);
+  BrowserTestUtils.removeTab(openTabForUsualLink);
+  BrowserTestUtils.removeTab(openTabForNonEditableLink);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/tabs/file_anchor_elements.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<head>
+  <meta charset="utf-8">
+  <title>Testing whether paste event is fired at middle click on anchor elements</title>
+</head>
+<body>
+  <p>Here is an <a id="a_with_href" href="http://example.com/#a_with_href">anchor element</a></p>
+  <p contenteditable>Here is an <a id="editable_a_with_href" href="http://example.com/#editable_a_with_href">editable anchor element</a></p>
+  <p contenteditable>Here is <span contenteditable="false"><a id="non-editable_a_with_href" href="http://example.com/#non-editable_a_with_href">non-editable anchor element</a></span>
+  <p>Here is an <a id="a_with_name" name="a_with_name">anchor element without href</a></p>
+</body>
+</html>
\ No newline at end of file
--- a/dom/events/Event.h
+++ b/dom/events/Event.h
@@ -216,16 +216,20 @@ class Event : public nsISupports, public
   bool DefaultPreventedByChrome() const {
     return mEvent->mFlags.mDefaultPreventedByChrome;
   }
 
   bool DefaultPreventedByContent() const {
     return mEvent->mFlags.mDefaultPreventedByContent;
   }
 
+  void PreventMultipleActions() {
+    mEvent->mFlags.mMultipleActionsPrevented = true;
+  }
+
   bool MultipleActionsPrevented() const {
     return mEvent->mFlags.mMultipleActionsPrevented;
   }
 
   bool ReturnValue(CallerType aCallerType) const;
 
   void SetReturnValue(bool aReturnValue, CallerType aCallerType);
 
--- a/dom/webidl/Event.webidl
+++ b/dom/webidl/Event.webidl
@@ -76,16 +76,17 @@ partial interface Event {
    * are retargeted to their parent node when they happen over text nodes (bug
    * 185889), and in that case .target will show the parent and
    * .explicitOriginalTarget will show the text node.
    * .explicitOriginalTarget differs from .originalTarget in that it will never
    * contain anonymous content.
    */
   readonly attribute EventTarget? explicitOriginalTarget;
   [ChromeOnly] readonly attribute EventTarget? composedTarget;
+  [ChromeOnly] void preventMultipleActions();
   [ChromeOnly] readonly attribute boolean multipleActionsPrevented;
   [ChromeOnly] readonly attribute boolean isSynthesized;
 };
 
 dictionary EventInit {
   boolean bubbles = false;
   boolean cancelable = false;
   boolean composed = false;