Bug 1352852 - Add dummy data to the DataTransfer when dragging a draggable object, r=enndeakin
authorMichael Layzell <michael@thelayzells.com>
Fri, 21 Apr 2017 16:38:12 -0400
changeset 569741 492fab8fac4e3a7773720d68898219aaff18e101
parent 569568 af2b7f5b3ab5a656c69c26caaadc3ca16eae799f
child 569742 bf1c55598c82e26cce1eadbccd066d0510823aee
push id56271
push userdmitchell@mozilla.com
push dateThu, 27 Apr 2017 22:16:15 +0000
reviewersenndeakin
bugs1352852
milestone55.0a1
Bug 1352852 - Add dummy data to the DataTransfer when dragging a draggable object, r=enndeakin When dragging a `draggable=true` HTML DOM node, if no data is added to the DataTransfer during the DragStart event, we currently cancel the drag. This is inconsistent with Chrome's behaviour. This patch adds a chrome-only (hidden from content) item to the DataTransfer: application/x-moz-dummy-data. This data is added only when no other data has been added to the DataTransfer, and the target of the dragstart event was a draggable=true HTML DOM node. This hidden node allows for the drag event to complete successfully, while appearing the same as Chrome's behavior to content scripts. MozReview-Commit-ID: HVqEr7aR6DD
dom/events/DataTransfer.cpp
dom/events/DataTransfer.h
dom/events/EventStateManager.cpp
dom/events/test/mochitest.ini
dom/events/test/test_drag_empty.html
widget/nsITransferable.idl
--- a/dom/events/DataTransfer.cpp
+++ b/dom/events/DataTransfer.cpp
@@ -33,16 +33,17 @@
 #include "mozilla/dom/DataTransferBinding.h"
 #include "mozilla/dom/DataTransferItemList.h"
 #include "mozilla/dom/Directory.h"
 #include "mozilla/dom/Element.h"
 #include "mozilla/dom/FileList.h"
 #include "mozilla/dom/BindingUtils.h"
 #include "mozilla/dom/OSFileSystem.h"
 #include "mozilla/dom/Promise.h"
+#include "mozilla/storage/Variant.h"
 #include "nsNetUtil.h"
 
 namespace mozilla {
 namespace dom {
 
 NS_IMPL_CYCLE_COLLECTION_CLASS(DataTransfer)
 
 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(DataTransfer)
@@ -1568,10 +1569,33 @@ DataTransfer::FillInExternalCustomTypes(
       rv = variant->SetAsAString(data);
       NS_ENSURE_SUCCESS_VOID(rv);
 
       SetDataWithPrincipal(format, variant, aIndex, aPrincipal);
     }
   } while (type != eCustomClipboardTypeId_None);
 }
 
+void
+DataTransfer::AddDummyHiddenData()
+{
+  RefPtr<nsVariantCC> variant = new nsVariantCC();
+  variant->SetAsAString(EmptyString());
+
+  ErrorResult rv;
+  RefPtr<DataTransferItem> item =
+    mItems->SetDataWithPrincipal(NS_LITERAL_STRING(kDummyTypeMime),
+                                 variant,
+                                 /* aIndex = */ 0,
+                                 nsContentUtils::GetSystemPrincipal(),
+                                 /* aInsertOnly = */ true,
+                                 /* aHidden = */ true,
+                                 rv);
+  MOZ_ASSERT(item->ChromeOnly());
+
+  // We might have gotten a NS_ERROR_DOM_NOT_SUPPORTED_ERROR because we've
+  // already added an application/x-moz-dummy-data. We want to ignore that
+  // error.
+  rv.SuppressException();
+}
+
 } // namespace dom
 } // namespace mozilla
--- a/dom/events/DataTransfer.h
+++ b/dom/events/DataTransfer.h
@@ -265,16 +265,20 @@ public:
   // Variation of SetDataWithPrincipal with handles extracting
   // kCustomTypesMime data into separate types.
   void SetDataWithPrincipalFromOtherProcess(const nsAString& aFormat,
                                             nsIVariant* aData,
                                             uint32_t aIndex,
                                             nsIPrincipal* aPrincipal,
                                             bool aHidden);
 
+  // Adds a dummy type to the DataTransfer which is hidden from the user to
+  // allow the drag to run. This type is not visible to content code.
+  void AddDummyHiddenData();
+
   // returns a weak reference to the drag image
   Element* GetDragImage(int32_t* aX, int32_t* aY) const
   {
     *aX = mDragImageX;
     *aY = mDragImageY;
     return mDragImage;
   }
 
--- a/dom/events/EventStateManager.cpp
+++ b/dom/events/EventStateManager.cpp
@@ -1793,16 +1793,33 @@ EventStateManager::GenerateDragGesture(n
 
       // Dispatch the dragstart event to the DOM.
       nsEventStatus status = nsEventStatus_eIgnore;
       EventDispatcher::Dispatch(targetContent, aPresContext, &startEvent,
                                 nullptr, &status);
 
       WidgetDragEvent* event = &startEvent;
 
+      // If we didn't get any data in the dataTransfer, yet the targetContent is
+      // a draggable HTML element draggable, we need to fire the drag event
+      // anyway. We'll add a custom dummy data type to the DataTransfer to make
+      // this possible. This data type will be marked as hidden so that content
+      // can't see it. This does not apply to non-HTML elements.
+      uint32_t count = 0;
+      dataTransfer->GetMozItemCount(&count);
+      if (count == 0) {
+        nsCOMPtr<nsIDOMHTMLElement> htmlDragTarget = do_QueryInterface(targetContent);
+        bool draggable = false;
+        if (htmlDragTarget &&
+            NS_SUCCEEDED(htmlDragTarget->GetDraggable(&draggable)) &&
+            draggable) {
+          dataTransfer->AddDummyHiddenData();
+        }
+      }
+
       nsCOMPtr<nsIObserverService> observerService =
         mozilla::services::GetObserverService();
       // Emit observer event to allow addons to modify the DataTransfer object.
       if (observerService) {
         observerService->NotifyObservers(dataTransfer,
                                          "on-datatransfer-available",
                                          nullptr);
       }
--- a/dom/events/test/mochitest.ini
+++ b/dom/events/test/mochitest.ini
@@ -172,8 +172,9 @@ skip-if = toolkit == 'android' #CRASH_DU
 [test_wheel_default_action.html]
 [test_bug687787.html]
 [test_bug1305458.html]
 [test_bug1298970.html]
 [test_bug1304044.html]
 [test_bug1332699.html]
 [test_bug1339758.html]
 [test_dnd_with_modifiers.html]
+[test_drag_empty.html]
new file mode 100644
--- /dev/null
+++ b/dom/events/test/test_drag_empty.html
@@ -0,0 +1,78 @@
+<html>
+<head>
+  <title>Tests for the dragstart event</title>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+  <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+
+<body>
+<!--
+ This test checks the behaviour of the dragstart event when no data is added to the DataTransfer.
+  -->
+  <style>
+    #droptarget { width: 100px; height: 100px; background-color: #550000; }
+  </style>
+
+<div id="draggable" draggable="true" ondragstart="onDragStart(event)">This is a bit of text.</div>
+
+<div id="droptarget" ondragover="onDragOver(event)" ondrop="onDrop(event)">THIS IS A DROP TARGET?!?!?</div>
+
+<script>
+  let dragStartFired = false;
+  function onDragStart(event) {
+    dragStartFired = true;
+    info("dragstart fired");
+  }
+
+  let dragOverFired = false;
+  function onDragOver(event) {
+    dragOverFired = true;
+    event.preventDefault();
+    info("dragover fired");
+  }
+
+  let dropFired = false;
+  function onDrop(event) {
+    dropFired = true;
+    is(event.dataTransfer.types.length, 0,
+       "There shouldn't be any data on the DataTransfer");
+    info("drop fired");
+  }
+
+  let loaded = new Promise(resolve => document.body.onload = resolve);
+
+  add_task(function*() {
+    var ds = SpecialPowers.Cc["@mozilla.org/widget/dragservice;1"].
+             getService(SpecialPowers.Ci.nsIDragService);
+    ok(!ds.getCurrentSession(), "There should be no drag session in progress");
+
+    // XXX: Make sure that we've finished loading the document before we start
+    // trying to do work with drag events.
+    yield loaded;
+
+    var draggable = $("draggable");
+    var droptarget = $("droptarget");
+    // Fire the dragstart event - this should start a drag session
+    synthesizeMouse(draggable, 6, 6, { type: "mousedown" });
+    synthesizeMouse(draggable, 14, 14, { type: "mousemove" });
+    // drags are asynchronous on Linux, so this extra event is needed to make
+    // sure the drag gets processed
+    synthesizeMouse(draggable, 15, 15, { type: "mousemove" });
+
+    ok(dragStartFired, "dragstart should have fired");
+    ok(ds.getCurrentSession(), "The drag session should have started");
+
+    // Synthesize the dragover and drop events on the target node.
+    var [result, dataTransfer] = synthesizeDragOver(draggable, droptarget);
+    synthesizeDropAfterDragOver(result, dataTransfer, droptarget);
+    ds.endDragSession(true);
+
+    ok(dropFired, "drop should have been fired");
+    ok(dragOverFired, "dragover should have been fired");
+    ok(!ds.getCurrentSession(), "The drag session should have ended");
+  });
+</script>
+</body>
+</html>
--- a/widget/nsITransferable.idl
+++ b/widget/nsITransferable.idl
@@ -45,18 +45,25 @@ interface nsIPrincipal;
 #define kFilePromiseURLMime         "application/x-moz-file-promise-url"
 // the destination filename for a file promise
 #define kFilePromiseDestFilename    "application/x-moz-file-promise-dest-filename"
 // a dataless flavor used to interact with the OS during file drags
 #define kFilePromiseMime            "application/x-moz-file-promise"
 // a synthetic flavor, put into the transferable once we know the destination directory of a file drag
 #define kFilePromiseDirectoryMime   "application/x-moz-file-promise-dir"
 
+// A custom data type used by DataTransfer for securely transferring arbitrary
+// data through the drag or clipboard object without allowing websites to set
+// arbitrary data types.
 #define kCustomTypesMime "application/x-moz-custom-clipdata"
 
+// A custom data type used during drag events to allow them to occur even when
+// no data has been attached to the drag event's Transferrable.
+#define kDummyTypeMime "application/x-moz-dummy-data"
+
 %}
 
 
 /**
   * nsIFlavorDataProvider allows a flavor to 'promise' data later,
   * supplying the data lazily.
   * 
   * To use it, call setTransferData, passing the flavor string,