Bug 92737 - Part 3: Open multiple tabs when multiple items are dropped on remote content area. r=enndeakin
authorTooru Fujisawa <arai_a@mac.com>
Wed, 11 Nov 2015 07:35:12 +0900
changeset 314592 978a6615d7cce1d5970fd014c72ec474dfa02629
parent 314591 1d44acbb76fe1ffcb52ce1d99ddd898b2c5ab5f8
child 314593 40e1dbd5409b5d7f54eb39c787d643af5139265f
push id30732
push usercbook@mozilla.com
push dateWed, 21 Sep 2016 10:04:03 +0000
treeherdermozilla-central@560b2c805bf7 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersenndeakin
bugs92737
milestone52.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 92737 - Part 3: Open multiple tabs when multiple items are dropped on remote content area. r=enndeakin
browser/base/content/browser.js
dom/interfaces/base/nsIRemoteBrowser.idl
dom/interfaces/base/nsITabChild.idl
dom/ipc/PBrowser.ipdl
dom/ipc/TabChild.cpp
dom/ipc/TabParent.cpp
dom/ipc/TabParent.h
dom/ipc/nsIBrowser.idl
embedding/browser/nsDocShellTreeOwner.cpp
toolkit/content/tests/chrome/window_browser_drop.xul
toolkit/content/widgets/browser.xml
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -5649,19 +5649,24 @@ function handleDroppedLink(event, urlOrL
   } else {
     links = [{ url: urlOrLinks, name, type: "" }];
   }
 
   let lastLocationChange = gBrowser.selectedBrowser.lastLocationChange;
 
   let userContextId = gBrowser.selectedBrowser.getAttribute("usercontextid");
 
-  let inBackground = Services.prefs.getBoolPref("browser.tabs.loadInBackground");
-  if (event.shiftKey)
-    inBackground = !inBackground;
+  // event is null if links are dropped in content process.
+  // inBackground should be false, as it's loading into current browser.
+  let inBackground = false;
+  if (event) {
+    let inBackground = Services.prefs.getBoolPref("browser.tabs.loadInBackground");
+    if (event.shiftKey)
+      inBackground = !inBackground;
+  }
 
   Task.spawn(function*() {
     let urls = [];
     let postDatas = [];
     for (let link of links) {
       let data = yield getShortcutOrURIAndPostData(link.url);
       urls.push(data.url);
       postDatas.push(data.postData);
@@ -5672,19 +5677,23 @@ function handleDroppedLink(event, urlOrL
         replace: true,
         allowThirdPartyFixup: false,
         postDatas,
         userContextId,
       });
     }
   });
 
-  // Keep the event from being handled by the dragDrop listeners
-  // built-in to gecko if they happen to be above us.
-  event.preventDefault();
+  // If links are dropped in content process, event.preventDefault() should be
+  // called in content process.
+  if (event) {
+    // Keep the event from being handled by the dragDrop listeners
+    // built-in to gecko if they happen to be above us.
+    event.preventDefault();
+  }
 }
 
 function BrowserSetForcedCharacterSet(aCharset)
 {
   if (aCharset) {
     gBrowser.selectedBrowser.characterSet = aCharset;
     // Save the forced character-set
     if (!PrivateBrowsingUtils.isWindowPrivate(window))
--- a/dom/interfaces/base/nsIRemoteBrowser.idl
+++ b/dom/interfaces/base/nsIRemoteBrowser.idl
@@ -18,9 +18,8 @@ interface nsIRemoteBrowser : nsISupports
    * @param disabledCommand commands to disable
    */
   void enableDisableCommands(in AString action,
                              in unsigned long enabledLength,
                              [array, size_is(enabledLength)] in string enabledCommands,
                              in unsigned long disabledLength,
                              [array, size_is(disabledLength)] in string disabledCommands);
 };
-
--- a/dom/interfaces/base/nsITabChild.idl
+++ b/dom/interfaces/base/nsITabChild.idl
@@ -1,14 +1,16 @@
 /* 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/. */
 
 
 #include "domstubs.idl"
+#include "nsIDroppedLinkHandler.idl"
+
 interface nsIContentFrameMessageManager;
 interface nsIWebBrowserChrome3;
 
 native CommandsArray(nsTArray<nsCString>);
 [ref] native CommandsArrayRef(nsTArray<nsCString>);
 
 [scriptable, uuid(1fb79c27-e760-4088-b19c-1ce3673ec24e)]
 interface nsITabChild : nsISupports
@@ -23,11 +25,14 @@ interface nsITabChild : nsISupports
 
   [noscript, notxpcom] void enableDisableCommands(in AString action,
                                                   in CommandsArrayRef enabledCommands,
                                                   in CommandsArrayRef disabledCommands);
 
   [noscript] void remoteSizeShellTo(in int32_t width, in int32_t height,
                                     in int32_t shellItemWidth, in int32_t shellItemHeight);
 
+  [noscript] void remoteDropLinks(in unsigned long linksCount,
+                                  [array, size_is(linksCount)] in nsIDroppedLinkItem links);
+
   readonly attribute uint64_t tabId;
 };
 
--- a/dom/ipc/PBrowser.ipdl
+++ b/dom/ipc/PBrowser.ipdl
@@ -181,16 +181,24 @@ parent:
      * aShellItemHeight On parent side we won't be able to decide the dimensions
      *                  of the shell item parameter in the original SizeShellTo
      *                  call so we send over its dimensions that will be used
      *                  for the actual resize.
      **/
     async SizeShellTo(uint32_t aFlag, int32_t aWidth, int32_t aHeight,
                       int32_t aShellItemWidth, int32_t aShellItemHeight);
 
+    /**
+     * Called by the child to inform the parent that links are dropped into
+     * content area.
+     *
+     * aLinks A flat array of url, name, and type for each link
+     */
+    async DropLinks(nsString[] aLinks);
+
     async Event(RemoteDOMEvent aEvent);
 
     sync SyncMessage(nsString aMessage, ClonedMessageData aData,
                      CpowEntry[] aCpows, Principal aPrincipal)
       returns (StructuredCloneData[] retval);
 
     prio(high) sync RpcMessage(nsString aMessage, ClonedMessageData aData,
                                CpowEntry[] aCpows, Principal aPrincipal)
--- a/dom/ipc/TabChild.cpp
+++ b/dom/ipc/TabChild.cpp
@@ -948,16 +948,47 @@ TabChild::RemoteSizeShellTo(int32_t aWid
   }
 
   bool sent = SendSizeShellTo(flags, aWidth, aHeight, aShellItemWidth, aShellItemHeight);
 
   return sent ? NS_OK : NS_ERROR_FAILURE;
 }
 
 NS_IMETHODIMP
+TabChild::RemoteDropLinks(uint32_t aLinksCount, nsIDroppedLinkItem** aLinks)
+{
+  nsTArray<nsString> linksArray;
+  nsresult rv = NS_OK;
+  for (uint32_t i = 0; i < aLinksCount; i++) {
+    nsString tmp;
+    rv = aLinks[i]->GetUrl(tmp);
+    if (NS_FAILED(rv)) {
+      return rv;
+    }
+    linksArray.AppendElement(tmp);
+
+    rv = aLinks[i]->GetName(tmp);
+    if (NS_FAILED(rv)) {
+      return rv;
+    }
+    linksArray.AppendElement(tmp);
+
+    rv = aLinks[i]->GetType(tmp);
+    if (NS_FAILED(rv)) {
+      return rv;
+    }
+    linksArray.AppendElement(tmp);
+  }
+
+  bool sent = SendDropLinks(linksArray);
+
+  return sent ? NS_OK : NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
 TabChild::SizeBrowserTo(int32_t aWidth, int32_t aHeight)
 {
   NS_WARNING("TabChild::SizeBrowserTo not supported in TabChild");
 
   return NS_ERROR_NOT_IMPLEMENTED;
 }
 
 NS_IMETHODIMP
--- a/dom/ipc/TabParent.cpp
+++ b/dom/ipc/TabParent.cpp
@@ -44,16 +44,17 @@
 #include "BlobParent.h"
 #include "nsCOMPtr.h"
 #include "nsContentAreaDragDrop.h"
 #include "nsContentUtils.h"
 #include "nsDebug.h"
 #include "nsFocusManager.h"
 #include "nsFrameLoader.h"
 #include "nsIBaseWindow.h"
+#include "nsIBrowser.h"
 #include "nsIContent.h"
 #include "nsIDocShell.h"
 #include "nsIDocShellTreeOwner.h"
 #include "nsIDOMElement.h"
 #include "nsIDOMEvent.h"
 #include "nsIDOMWindow.h"
 #include "nsIDOMWindowUtils.h"
 #include "nsIInterfaceRequestorUtils.h"
@@ -693,16 +694,31 @@ TabParent::RecvSizeShellTo(const uint32_
   nsCOMPtr<nsIXULWindow> xulWin(do_GetInterface(treeOwner));
   NS_ENSURE_TRUE(xulWin, true);
   xulWin->SizeShellToWithLimit(width, height, aShellItemWidth, aShellItemHeight);
 
   return true;
 }
 
 bool
+TabParent::RecvDropLinks(nsTArray<nsString>&& aLinks)
+{
+  nsCOMPtr<nsIBrowser> browser = do_QueryInterface(mFrameElement);
+  if (browser) {
+    UniquePtr<const char16_t*[]> links;
+    links = MakeUnique<const char16_t*[]>(aLinks.Length());
+    for (uint32_t i = 0; i < aLinks.Length(); i++) {
+      links[i] = aLinks[i].get();
+    }
+    browser->DropLinks(aLinks.Length(), links.get());
+  }
+  return true;
+}
+
+bool
 TabParent::RecvEvent(const RemoteDOMEvent& aEvent)
 {
   nsCOMPtr<nsIDOMEvent> event = do_QueryInterface(aEvent.mEvent);
   NS_ENSURE_TRUE(event, true);
 
   nsCOMPtr<mozilla::dom::EventTarget> target = do_QueryInterface(mFrameElement);
   NS_ENSURE_TRUE(target, true);
 
--- a/dom/ipc/TabParent.h
+++ b/dom/ipc/TabParent.h
@@ -165,16 +165,18 @@ public:
                              const bool& aForDocumentNavigation) override;
 
   virtual bool RecvSizeShellTo(const uint32_t& aFlags,
                                const int32_t& aWidth,
                                const int32_t& aHeight,
                                const int32_t& aShellItemWidth,
                                const int32_t& aShellItemHeight) override;
 
+  virtual bool RecvDropLinks(nsTArray<nsString>&& aLinks) override;
+
   virtual bool RecvEvent(const RemoteDOMEvent& aEvent) override;
 
   virtual bool RecvReplyKeyEvent(const WidgetKeyboardEvent& aEvent) override;
 
   virtual bool
   RecvDispatchAfterKeyboardEvent(const WidgetKeyboardEvent& aEvent) override;
 
   virtual bool
--- a/dom/ipc/nsIBrowser.idl
+++ b/dom/ipc/nsIBrowser.idl
@@ -9,9 +9,19 @@ interface nsIDOMElement;
 interface nsIBrowser : nsISupports
 {
   /**
    * Gets a related browser for a given browser (if any). If this exists, then
    * we should attempt to use the same content parent as its frameLoader
    * for any new tab parents.
    */
   readonly attribute nsIDOMElement relatedBrowser;
+
+  /*
+   * Called by the child to inform the parent that links are dropped into
+   * content area.
+   *
+   * @param linksCount length of links
+   * @param links a flat array of url, name, and type for each link
+   */
+  void dropLinks(in unsigned long linksCount,
+                 [array, size_is(linksCount)] in wstring links);
 };
--- a/embedding/browser/nsDocShellTreeOwner.cpp
+++ b/embedding/browser/nsDocShellTreeOwner.cpp
@@ -1057,21 +1057,44 @@ nsDocShellTreeOwner::HandleEvent(nsIDOME
       bool canDropLink = false;
       handler->CanDropLink(dragEvent, false, &canDropLink);
       if (canDropLink) {
         aEvent->PreventDefault();
       }
     } else if (eventType.EqualsLiteral("drop")) {
       nsIWebNavigation* webnav = static_cast<nsIWebNavigation*>(mWebBrowser);
 
-      nsAutoString link, name;
+      uint32_t linksCount;
+      nsIDroppedLinkItem** links;
       if (webnav &&
-          NS_SUCCEEDED(handler->DropLink(dragEvent, name, true, link))) {
-        if (!link.IsEmpty()) {
-          webnav->LoadURI(link.get(), 0, nullptr, nullptr, nullptr);
+          NS_SUCCEEDED(handler->DropLinks(dragEvent, true, &linksCount, &links))) {
+        if (linksCount >= 1) {
+          nsCOMPtr<nsIWebBrowserChrome> webBrowserChrome = GetWebBrowserChrome();
+          if (webBrowserChrome) {
+            nsCOMPtr<nsITabChild> tabChild = do_QueryInterface(webBrowserChrome);
+            if (tabChild) {
+              nsresult rv = tabChild->RemoteDropLinks(linksCount, links);
+              for (uint32_t i = 0; i < linksCount; i++) {
+                NS_RELEASE(links[i]);
+              }
+              free(links);
+              return rv;
+            }
+          }
+          nsAutoString url;
+          if (NS_SUCCEEDED(links[0]->GetUrl(url))) {
+            if (!url.IsEmpty()) {
+              webnav->LoadURI(url.get(), 0, nullptr, nullptr, nullptr);
+            }
+          }
+
+          for (uint32_t i = 0; i < linksCount; i++) {
+            NS_RELEASE(links[i]);
+          }
+          free(links);
         }
       } else {
         aEvent->StopPropagation();
         aEvent->PreventDefault();
       }
     }
   }
 
--- a/toolkit/content/tests/chrome/window_browser_drop.xul
+++ b/toolkit/content/tests/chrome/window_browser_drop.xul
@@ -9,152 +9,237 @@
   <script type="application/javascript"
           src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
 
 <script>
 <![CDATA[
 
 Components.utils.import("resource://testing-common/ContentTask.jsm");
 
-function doDropAndStopLoad(browser, data, shouldExpectStateChange) {
+function dropOnRemoteBrowserAsync(browser, data, shouldExpectStateChange) {
   ContentTask.setTestScope(window); // Need this so is/isnot/ok are available inside the contenttask
   return ContentTask.spawn(browser, {data, shouldExpectStateChange}, function*({data, shouldExpectStateChange}) {
     let { interfaces: Ci, utils: Cu } = Components;
     Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
-    let nameReturned = "";
-    let uri = "";
-    let gotLoad = false;
-    function stopContent(uriLoaded) {
-      content.stop();
-      gotLoad = true;
-      uri = uriLoaded;
+    if (!content.document.documentElement) {
+      // Wait until the testing document gets loaded.
+      yield new Promise(resolve => {
+        let onload = function() {
+          content.window.removeEventListener("load", onload);
+          resolve();
+        };
+        content.window.addEventListener("load", onload);
+      });
     }
 
-    let wp = docShell.QueryInterface(Ci.nsIWebProgress);
-    let progressListener = {
-      onStateChange: function (webProgress, req, flags, status) {
-        info("waitForDocLoadAndStopIt: onStateChange " + flags.toString(16) + ": " + req.name + "\n");
-
-        if (webProgress.isTopLevel &&
-            flags & Ci.nsIWebProgressListener.STATE_START) {
-          wp.removeProgressListener(progressListener);
-
-          let chan = req.QueryInterface(Ci.nsIChannel);
-          info(`waitForDocLoadAndStopIt: Document start: ${chan.URI.spec}\n`);
-
-          stopContent(chan.originalURI.spec);
-        }
-      },
-      QueryInterface: XPCOMUtils.generateQI(["nsISupportsWeakReference"])
-    };
-    wp.addProgressListener(progressListener, wp.NOTIFY_STATE_WINDOW);
-
     let dataTransfer = new content.DataTransfer("dragstart", false);
-    dataTransfer.mozSetDataAt(data.type, data.data, 0);
+    for (let i = 0; i < data.length; i++) {
+      let types = data[i];
+      for (let j = 0; j < types.length; j++) {
+        dataTransfer.mozSetDataAt(types[j].type, types[j].data, i);
+      }
+    }
     let event = content.document.createEvent("DragEvent");
     event.initDragEvent("drop", true, true, content, 0, 0, 0, 0, 0,
                         false, false, false, false, 0, null, dataTransfer);
     content.document.body.dispatchEvent(event);
-    let nameGetter = {};
+
+    let links = [];
     try {
-      Services.droppedLinkHandler.dropLink(event, nameGetter, true);
-      nameReturned = nameGetter.value;
+      links = Services.droppedLinkHandler.dropLinks(event, true);
     } catch (ex) {
       if (shouldExpectStateChange) {
         ok(false, "Should not have gotten an exception from the dropped link handler, but got: " + ex);
         Cu.reportError(ex);
       }
     }
-    is(shouldExpectStateChange, gotLoad, "Should have gotten a load only if we expected it.");
-    if (!gotLoad) {
-      wp.removeProgressListener(progressListener);
-    }
-    return [uri, nameReturned];
+
+    return links;
   });
 }
 
-function expectLink(browser, expectedLink, expectedName, data, testid, onbody=false) {
-  let lastLink = "";
-  let lastLinkName = "";
+function* expectLink(browser, expectedLinks, data, testid, onbody=false) {
+  let lastLinks = [];
+  let lastLinksPromise = new Promise(resolve => {
+    browser.droppedLinkHandler = function(event, links) {
+      info(`droppedLinkHandler called, received links ${JSON.stringify(links)}`);
+      if (expectedLinks.length == 0) {
+        ok(false, `droppedLinkHandler called for ${JSON.stringify(links)} which we didn't expect.`);
+      }
+      lastLinks = links;
+      resolve(links);
+    };
+  });
+
   function dropOnBrowserSync() {
-    browser.droppedLinkHandler = function(event, link, linkname) {
-      info(`droppedLinkHandler called, received link ${link} and linkname ${linkname}`);
-      if (!expectedLink && !expectedName) {
-        ok(false, "droppedLinkHandler called for URI " + link + " which we didn't expect.");
-      }
-      lastLink = link;
-      lastLinkName = linkname;
-    };
-
     let dropEl = onbody ? browser.contentDocument.body : browser;
     synthesizeDrop(dropEl, dropEl, data, "", dropEl.ownerDocument.defaultView);
-    return Promise.resolve([lastLink, lastLinkName]);
   }
-  let dropInfoReceived;
+  let links;
   if (browser.isRemoteBrowser) {
-    dropInfoReceived = doDropAndStopLoad(browser, data[0][0], !!expectedLink);
+    let remoteLinks = yield dropOnRemoteBrowserAsync(browser, data, expectedLinks.length != 0);
+    is(remoteLinks.length, expectedLinks.length, testid + " remote links length");
+    for (let i = 0, length = remoteLinks.length; i < length; i++) {
+      is(remoteLinks[i].url, expectedLinks[i].url, testid + "[" + i + "] remote link");
+      is(remoteLinks[i].name, expectedLinks[i].name, testid + "[" + i + "] remote name");
+    }
+
+    if (expectedLinks.length == 0) {
+      // There is no way to check if nothing happens asynchronously.
+      return;
+    }
+
+    links = yield lastLinksPromise;
   } else {
-    dropInfoReceived = dropOnBrowserSync();
+    dropOnBrowserSync();
+    links = lastLinks;
   }
-  return dropInfoReceived.then(([uri, linkname]) => {
-    is(uri, expectedLink, testid + " link");
-    is(linkname, expectedName, testid + " name");
-  });
-}
+
+  is(links.length, expectedLinks.length, testid + " links length");
+  for (let i = 0, length = links.length; i < length; i++) {
+    is(links[i].url, expectedLinks[i].url, testid + "[" + i + "] link");
+    is(links[i].name, expectedLinks[i].name, testid + "[" + i + "] name");
+  }
+};
 
 function* dropLinksOnBrowser(browser, type) {
-  yield expectLink(browser, "http://www.mozilla.org/", "http://www.mozilla.org/",
-                   [ [ { type: "text/plain", data: "http://www.mozilla.org/" } ] ],
-                   "text/plain drop on browser " + type);
-  yield expectLink(browser, "", "",
-                   [ [ { type: "text/link", data: "http://www.mozilla.org/" } ] ],
-                   "text/link drop on browser " + type);
-  yield expectLink(browser, "http://www.example.com/", "http://www.example.com/",
-                   [ [ { type: "text/uri-list", data: "http://www.example.com/\nhttp://www.mozilla.org" } ] ],
-                   "text/uri-list drop on browser " + type);
-  yield expectLink(browser, "http://www.example.com/", "Example.com",
-                   [ [ { type: "text/x-moz-url", data: "http://www.example.com/\nExample.com" } ] ],
-                   "text/x-moz-url drop on browser " + type);
+  // Dropping single text/plain item with single link should open single
+  // page.
+  yield* expectLink(browser,
+                    [ { url: "http://www.mozilla.org/",
+                        name: "http://www.mozilla.org/" } ],
+                    [ [ { type: "text/plain",
+                          data: "http://www.mozilla.org/" } ] ],
+                    "text/plain drop on browser " + type);
+
+  // Dropping single text/plain item with multiple links should open
+  // multiple pages.
+  yield* expectLink(browser,
+                    [ { url: "http://www.mozilla.org/",
+                        name: "http://www.mozilla.org/" },
+                      { url: "http://www.example.com/",
+                        name: "http://www.example.com/" } ],
+                    [ [ { type: "text/plain",
+                          data: "http://www.mozilla.org/\nhttp://www.example.com/" } ] ],
+                    "text/plain with 2 URLs drop on browser " + type);
+
+  // Dropping sinlge unsupported type item should not open anything.
+  yield* expectLink(browser,
+                    [],
+                    [ [ { type: "text/link",
+                          data: "http://www.mozilla.org/" } ] ],
+                    "text/link drop on browser " + type);
+
+  // Dropping single text/uri-list item with single link should open single
+  // page.
+  yield* expectLink(browser,
+                    [ { url: "http://www.example.com/",
+                        name: "http://www.example.com/" } ],
+                    [ [ { type: "text/uri-list",
+                          data: "http://www.example.com/" } ] ],
+                    "text/uri-list drop on browser " + type);
+
+  // Dropping single text/uri-list item with multiple links should open
+  // multiple pages.
+  yield* expectLink(browser,
+                    [ { url: "http://www.example.com/",
+                        name: "http://www.example.com/" },
+                      { url: "http://www.mozilla.org/",
+                        name: "http://www.mozilla.org/" }],
+                    [ [ { type: "text/uri-list",
+                          data: "http://www.example.com/\nhttp://www.mozilla.org/" } ] ],
+                    "text/uri-list with 2 URLs drop on browser " + type);
+
+  // Name in text/x-moz-url should be handled.
+  yield* expectLink(browser,
+                    [ { url: "http://www.example.com/",
+                        name: "Example.com" } ],
+                    [ [ { type: "text/x-moz-url",
+                          data: "http://www.example.com/\nExample.com" } ] ],
+                    "text/x-moz-url drop on browser " + type);
+
+  yield* expectLink(browser,
+                    [ { url: "http://www.mozilla.org/",
+                        name: "Mozilla.org" },
+                      { url: "http://www.example.com/",
+                        name: "Example.com" } ],
+                    [ [ { type: "text/x-moz-url",
+                          data: "http://www.mozilla.org/\nMozilla.org\nhttp://www.example.com/\nExample.com" } ] ],
+                    "text/x-moz-url with 2 URLs drop on browser " + type);
+
+  // Dropping multiple items should open multiple pages.
+  yield* expectLink(browser,
+                    [ { url: "http://www.example.com/",
+                        name: "Example.com" },
+                      { url: "http://www.mozilla.org/",
+                        name: "http://www.mozilla.org/" }],
+                    [ [ { type: "text/x-moz-url",
+                          data: "http://www.example.com/\nExample.com" } ],
+                      [ { type: "text/plain",
+                          data: "http://www.mozilla.org/" } ] ],
+                    "text/x-moz-url and text/plain drop on browser " + type);
+
+  // Dropping single item with multiple types should open single page.
+  yield* expectLink(browser,
+                    [ { url: "http://www.example.org/",
+                        name: "Example.com" } ],
+                    [ [ { type: "text/plain",
+                          data: "http://www.mozilla.org/" },
+                        { type: "text/x-moz-url",
+                          data: "http://www.example.org/\nExample.com" } ] ],
+                    "text/plain and text/x-moz-url drop on browser " + type);
 
   // Dropping javascript or data: URLs should fail:
-  yield expectLink(browser, "", "",
-                   [ [ { type: "text/plain", data: "javascript:'bad'" } ] ],
-                   "text/plain javascript url drop on browser " + type);
-  yield expectLink(browser, "", "",
-                   [ [ { type: "text/plain", data: "jAvascript:'also bad'" } ] ],
-                   "text/plain mixed-case javascript url drop on browser " + type);
-  yield expectLink(browser, "", "",
-                   [ [ { type: "text/plain", data: "data:text/html,bad" } ] ],
-                   "text/plain data url drop on browser " + type);
+  yield* expectLink(browser,
+                    [],
+                    [ [ { type: "text/plain",
+                          data: "javascript:'bad'" } ] ],
+                    "text/plain javascript url drop on browser " + type);
+  yield* expectLink(browser,
+                    [],
+                    [ [ { type: "text/plain",
+                          data: "jAvascript:'also bad'" } ] ],
+                    "text/plain mixed-case javascript url drop on browser " + type);
+  yield* expectLink(browser,
+                    [],
+                    [ [ { type: "text/plain",
+                          data: "data:text/html,bad" } ] ],
+                    "text/plain data url drop on browser " + type);
 
-  // dropping a chrome url should fail as we don't have a source node set,
+  // Dropping a chrome url should fail as we don't have a source node set,
   // defaulting to a source of file:///
-  yield expectLink(browser, "", "",
-                   [ [ { type: "text/x-moz-url", data: "chrome://browser/content/browser.xul" } ] ],
-                   "text/x-moz-url chrome url drop on browser " + type);
+  yield* expectLink(browser,
+                    [],
+                    [ [ { type: "text/x-moz-url",
+                          data: "chrome://browser/content/browser.xul" } ] ],
+                    "text/x-moz-url chrome url drop on browser " + type);
 
   if (browser.type == "content") {
     yield ContentTask.spawn(browser, null, function() {
       content.window.stopMode = true;
     });
 
     // stopPropagation should not prevent the browser link handling from occuring
-    yield expectLink(browser, "http://www.mozilla.org/", "http://www.mozilla.org/",
-                     [ [ { type: "text/uri-list", data: "http://www.mozilla.org/" } ] ],
-                     "text/x-moz-url drop on browser with stopPropagation drop event", true);
+    yield* expectLink(browser,
+                      [ { url: "http://www.mozilla.org/",
+                          name: "http://www.mozilla.org/" } ],
+                      [ [ { type: "text/uri-list",
+                            data: "http://www.mozilla.org/" } ] ],
+                      "text/x-moz-url drop on browser with stopPropagation drop event", true);
 
     yield ContentTask.spawn(browser, null, function() {
       content.window.cancelMode = true;
     });
 
-    // canceling the event, however, should prevent the link from being handled
-    yield expectLink(browser, "", "",
-                     [ [ { type: "text/uri-list", data: "http://www.mozilla.org/" } ] ],
-                     "text/x-moz-url drop on browser with cancelled drop event", true);
+    // Canceling the event, however, should prevent the link from being handled.
+    yield* expectLink(browser,
+                      [],
+                      [ [ { type: "text/uri-list", data: "http://www.mozilla.org/" } ] ],
+                      "text/x-moz-url drop on browser with cancelled drop event", true);
   }
 }
 
 function info(msg) { window.opener.wrappedJSObject.SimpleTest.info(msg); }
 function is(l, r, n) { window.opener.wrappedJSObject.SimpleTest.is(l,r,n); }
 function ok(v, n) { window.opener.wrappedJSObject.SimpleTest.ok(v,n); }
 
 ]]>
--- a/toolkit/content/widgets/browser.xml
+++ b/toolkit/content/widgets/browser.xml
@@ -1331,16 +1331,36 @@
                                          Components.results.NS_ERROR_FAILURE);
             }
 
             owner.frameLoader.print(aOuterWindowID, aPrintSettings,
                                     aPrintProgressListener);
           ]]>
         </body>
       </method>
+
+      <method name="dropLinks">
+        <parameter name="aLinksCount"/>
+        <parameter name="aLinks"/>
+        <body><![CDATA[
+          if (!this.droppedLinkHandler) {
+            return false;
+          }
+          let links = [];
+          for (let i = 0; i < aLinksCount; i += 3) {
+            links.push({
+              url: aLinks[i],
+              name: aLinks[i + 1],
+              type: aLinks[i + 2],
+            });
+          }
+          this.droppedLinkHandler(null, links);
+          return true;
+        ]]></body>
+      </method>
     </implementation>
 
     <handlers>
       <handler event="keypress" keycode="VK_F7" group="system">
         <![CDATA[
           if (event.defaultPrevented || !event.isTrusted)
             return;