Bug 1800972 [Linux] Implement drag of multiple items r=emilio

- Remove nsDragService::CreateURIList() and replace it by nsDragService::SourceDataGetUriList().
  It reads all items from nsITransferable and put them to uri list.
  If data drop is performed to another application which doesn't have access to internal data storages (mailbox:// for instance),
  request download and save referenced items to /tmp directory.

- Implement SourceDataAppendURLItem() which read one item from nsITransferable and append it to uri list.
  Download and store internal files in /tmp directory.

- Make CreateTempFile() to block native events processing. nsIOutputStream/nsIInputStream read and write operations run
  event loop and can abort D&D operation before we write all data to /tmp. Use nsAppShell to block native event processing
  during read/write to prevent it.

Differential Revision: https://phabricator.services.mozilla.com/D162473
This commit is contained in:
stransky 2022-11-22 08:14:27 +00:00
parent ecd42fb817
commit d02fafe749
3 changed files with 179 additions and 116 deletions

View File

@ -420,6 +420,9 @@ void nsAppShell::ScheduleNativeEventCallback() {
}
bool nsAppShell::ProcessNextNativeEvent(bool mayWait) {
if (mSuspendNativeCount) {
return false;
}
bool didProcessEvent = g_main_context_iteration(nullptr, mayWait);
#ifdef MOZ_WAYLAND
mozilla::widget::WaylandDispatchDisplays();

View File

@ -32,6 +32,7 @@
#include "mozilla/AutoRestore.h"
#include "mozilla/WidgetUtilsGtk.h"
#include "GRefPtr.h"
#include "nsAppShell.h"
#ifdef MOZ_X11
# include "gfxXlibSurface.h"
@ -496,7 +497,7 @@ bool nsDragService::SetAlphaPixmap(SourceSurface* aSurface,
NS_IMETHODIMP
nsDragService::StartDragSession() {
LOGDRAGSERVICE("nsDragService::StartDragSession");
mTempFileUrl.Truncate();
mTempFileUrls.Clear();
return nsBaseDragService::StartDragSession();
}
@ -566,7 +567,7 @@ nsDragService::EndDragSession(bool aDoneDrag, uint32_t aKeyModifiers) {
// removed in the nsDragService destructor.
mTempFileTimerID =
g_timeout_add(NS_DND_TMP_CLEANUP_TIMEOUT, TaskRemoveTempFiles, this);
mTempFileUrl.Truncate();
mTempFileUrls.Clear();
}
// We're done with the drag context.
@ -1296,7 +1297,7 @@ static void TargetArrayAddTarget(nsTArray<GtkTargetEntry*>& aTargetArray,
LOGDRAGSERVICESTATIC("adding target %s\n", aTarget);
}
static bool CanExportAsURLTarget(char16_t* aURLData, uint32_t aURLLen) {
static bool CanExportAsURLTarget(const char16_t* aURLData, uint32_t aURLLen) {
for (const nsLiteralString& disallowed : kDisallowedExportedSchemes) {
auto len = disallowed.AsString().Length();
if (len < aURLLen) {
@ -1528,57 +1529,6 @@ void nsDragService::SourceEndDragSession(GdkDragContext* aContext,
Schedule(eDragTaskSourceEnd, nullptr, nullptr, LayoutDeviceIntPoint(), 0);
}
static void CreateURIList(nsIArray* aItems, nsACString& aURIList) {
uint32_t length = 0;
aItems->GetLength(&length);
for (uint32_t i = 0; i < length; ++i) {
nsCOMPtr<nsITransferable> item = do_QueryElementAt(aItems, i);
if (!item) {
continue;
}
nsCOMPtr<nsISupports> data;
nsresult rv = item->GetTransferData(kURLMime, getter_AddRefs(data));
if (NS_SUCCEEDED(rv)) {
nsCOMPtr<nsISupportsString> string = do_QueryInterface(data);
nsAutoString text;
if (string) {
string->GetData(text);
}
// text/x-moz-url is of form url + "\n" + title.
// We just want the url.
int32_t separatorPos = text.FindChar(u'\n');
if (separatorPos >= 0) {
text.Truncate(separatorPos);
}
AppendUTF16toUTF8(text, aURIList);
aURIList.AppendLiteral("\r\n");
continue;
}
// There is no URI available. If there is a file available, create
// a URI from the file.
rv = item->GetTransferData(kFileMime, getter_AddRefs(data));
if (NS_SUCCEEDED(rv)) {
if (nsCOMPtr<nsIFile> file = do_QueryInterface(data)) {
nsCOMPtr<nsIURI> fileURI;
NS_NewFileURI(getter_AddRefs(fileURI), file);
if (fileURI) {
nsAutoCString spec;
fileURI->GetSpec(spec);
aURIList.Append(spec);
aURIList.AppendLiteral("\r\n");
}
}
}
}
}
static nsresult GetDownloadDetails(nsITransferable* aTransferable,
nsIURI** aSourceURI, nsAString& aFilename) {
*aSourceURI = nullptr;
@ -1636,12 +1586,11 @@ static nsresult GetDownloadDetails(nsITransferable* aTransferable,
// See nsContentAreaDragDropDataProvider::GetFlavorData() for reference.
nsresult nsDragService::CreateTempFile(nsITransferable* aItem,
GtkSelectionData* aSelectionData) {
nsACString& aURI) {
LOGDRAGSERVICE("nsDragService::CreateTempFile()");
nsCOMPtr<nsIFile> tmpDir;
nsresult rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(tmpDir));
if (NS_FAILED(rv)) {
LOGDRAGSERVICE(" Failed to get temp directory\n");
return rv;
@ -1660,6 +1609,22 @@ nsresult nsDragService::CreateTempFile(nsITransferable* aItem,
return rv;
}
// Check if the file is already stored at /tmp.
// It happens when drop destination is changed and SourceDataGet() is caled
// more than once.
nsAutoCString fileName;
CopyUTF16toUTF8(wideFileName, fileName);
auto fileLen = fileName.Length();
for (const auto& url : mTempFileUrls) {
auto URLLen = url.Length();
if (URLLen > fileLen &&
fileName.Equals(nsDependentCString(url, URLLen - fileLen))) {
aURI = url;
LOGDRAGSERVICE(" recycle file %s", PromiseFlatCString(aURI).get());
return NS_OK;
}
}
// create and open channel for source uri
nsCOMPtr<nsIPrincipal> principal = aItem->GetRequestingPrincipal();
nsContentPolicyType contentPolicyType = aItem->GetContentPolicyType();
@ -1707,6 +1672,15 @@ nsresult nsDragService::CreateTempFile(nsITransferable* aItem,
return rv;
}
// Stream Read()/Write() runs event loop which process system event
// so during Read()/Write() we can get another D&D event.
// Such event is handled before we finish our write here and
// it leads to cancelled D&D operation.
// So suspend native event processing temporary.
nsCOMPtr<nsIAppShell> appShell = do_GetService(NS_APPSHELL_CID);
appShell->SuspendNative();
auto resumeEvents = MakeScopeExit([&] { appShell->ResumeNative(); });
char buffer[8192];
uint32_t readCount = 0;
uint32_t writeCount = 0;
@ -1735,21 +1709,128 @@ nsresult nsDragService::CreateTempFile(nsITransferable* aItem,
nsCOMPtr<nsIURI> uri;
rv = NS_NewFileURI(getter_AddRefs(uri), tmpDir);
if (NS_SUCCEEDED(rv)) {
nsCOMPtr<nsIURL> fileURL(do_QueryInterface(uri));
if (fileURL) {
nsAutoCString urltext;
rv = fileURL->GetSpec(urltext);
if (NS_SUCCEEDED(rv)) {
// store url of temporary file
LOGDRAGSERVICE(" storing tmp file as %s", urltext.get());
mTempFileUrl = urltext;
return NS_OK;
}
}
if (NS_FAILED(rv)) {
LOGDRAGSERVICE(" Failed to get file URI");
return rv;
}
nsCOMPtr<nsIURL> fileURL(do_QueryInterface(uri));
if (!fileURL) {
LOGDRAGSERVICE(" Failed to query file interface");
return NS_ERROR_FAILURE;
}
rv = fileURL->GetSpec(aURI);
if (NS_FAILED(rv)) {
LOGDRAGSERVICE(" Failed to get filepath");
return rv;
}
return NS_ERROR_FAILURE;
// store url of temporary file
mTempFileUrls.AppendElement()->Assign(aURI);
LOGDRAGSERVICE(" storing tmp file as %s", PromiseFlatCString(aURI).get());
return NS_OK;
}
bool nsDragService::SourceDataAppendURLFileItem(nsACString& aURI,
nsITransferable* aItem) {
// If there is a file available, create a URI from the file.
nsCOMPtr<nsISupports> data;
nsresult rv = aItem->GetTransferData(kFileMime, getter_AddRefs(data));
NS_ENSURE_SUCCESS(rv, false);
if (nsCOMPtr<nsIFile> file = do_QueryInterface(data)) {
nsCOMPtr<nsIURI> fileURI;
NS_NewFileURI(getter_AddRefs(fileURI), file);
if (fileURI) {
fileURI->GetSpec(aURI);
return true;
}
}
return false;
}
bool nsDragService::SourceDataAppendURLItem(nsITransferable* aItem,
bool aExternalDrop,
nsACString& aURI) {
nsCOMPtr<nsISupports> data;
nsresult rv = aItem->GetTransferData(kURLMime, getter_AddRefs(data));
if (NS_FAILED(rv)) {
return SourceDataAppendURLFileItem(aURI, aItem);
}
nsCOMPtr<nsISupportsString> string = do_QueryInterface(data);
if (!string) {
return false;
}
nsAutoString text;
string->GetData(text);
if (!aExternalDrop || CanExportAsURLTarget(text.get(), text.Length())) {
AppendUTF16toUTF8(text, aURI);
return true;
}
// We're dropping to another application and the URL can't be exported
// as it's internal one (mailbox:// etc.)
// Try to get file target directly.
if (SourceDataAppendURLFileItem(aURI, aItem)) {
return true;
}
// We can't get the file directly so try to download it and save to tmp.
// The desktop or file manager expects for drags of promise-file data
// the text/uri-list flavor set to a temporary file that contains the
// promise-file data.
// We open a stream on the <protocol>:// url here and save the content
// to file:///tmp/dnd_file/<filename> and pass this url
// as text/uri-list flavor.
// check whether transferable contains FilePromiseUrl flavor...
nsCOMPtr<nsISupports> promiseData;
rv = aItem->GetTransferData(kFilePromiseURLMime, getter_AddRefs(promiseData));
NS_ENSURE_SUCCESS(rv, false);
// ... if so, create a temporary file and pass its url
return NS_SUCCEEDED(CreateTempFile(aItem, aURI));
}
void nsDragService::SourceDataGetUriList(GdkDragContext* aContext,
GtkSelectionData* aSelectionData,
uint32_t aDragItems) {
// Check if we're transfering data to another application.
// gdk_drag_context_get_dest_window() on X11 returns GdkWindow even for
// different application so use nsWindow::GetWindow() to check if that's
// our window.
const bool isExternalDrop =
widget::GdkIsX11Display()
? !nsWindow::GetWindow(gdk_drag_context_get_dest_window(aContext))
: !gdk_drag_context_get_dest_window(aContext);
LOGDRAGSERVICE("nsDragService::SourceDataGetUriLists() len %d external %d",
aDragItems, isExternalDrop);
nsAutoCString uriList;
for (uint32_t i = 0; i < aDragItems; i++) {
nsCOMPtr<nsITransferable> item = do_QueryElementAt(mSourceDataItems, i);
if (!item) {
continue;
}
nsAutoCString uri;
if (!SourceDataAppendURLItem(item, isExternalDrop, uri)) {
continue;
}
// text/x-moz-url is of form url + "\n" + title.
// We just want the url.
int32_t separatorPos = uri.FindChar(u'\n');
if (separatorPos >= 0) {
uri.Truncate(separatorPos);
}
uriList.Append(uri);
uriList.AppendLiteral("\r\n");
}
LOGDRAGSERVICE("URI list\n%s", uriList.get());
GdkAtom target = gtk_selection_data_get_target(aSelectionData);
gtk_selection_data_set(aSelectionData, target, 8, (guchar*)uriList.get(),
uriList.Length());
}
void nsDragService::SourceDataGetImage(nsITransferable* aItem,
@ -1945,19 +2026,30 @@ void nsDragService::SourceDataGet(GtkWidget* aWidget, GdkDragContext* aContext,
return;
}
nsCOMPtr<nsITransferable> item;
item = do_QueryElementAt(mSourceDataItems, 0);
if (!item) {
uint32_t dragItems;
mSourceDataItems->GetLength(&dragItems);
LOGDRAGSERVICE(" source data items %d", dragItems);
nsDependentCString mimeFlavor(requestedTypeName.get());
if (mimeFlavor.EqualsLiteral(gTextUriListType)) {
SourceDataGetUriList(aContext, aSelectionData, dragItems);
return;
}
#ifdef MOZ_LOGGING
PRUint32 dragItems;
mSourceDataItems->GetLength(&dragItems);
LOGDRAGSERVICE(" source data items %d", dragItems);
if (dragItems > 1) {
LOGDRAGSERVICE(
" There are %d data items but we're asked for %s MIME type. Only "
"first data element can be transfered!",
dragItems, mimeFlavor.get());
}
#endif
nsDependentCString mimeFlavor(requestedTypeName.get());
nsCOMPtr<nsITransferable> item = do_QueryElementAt(mSourceDataItems, 0);
if (!item) {
LOGDRAGSERVICE(" Failed to get SourceDataItems!");
return;
}
if (mimeFlavor.EqualsLiteral(kTextMime) ||
mimeFlavor.EqualsLiteral(gTextPlainUTF8Type)) {
@ -1966,43 +2058,6 @@ void nsDragService::SourceDataGet(GtkWidget* aWidget, GdkDragContext* aContext,
aSelectionData);
// no fallback for text mime types
return;
} else if (mimeFlavor.EqualsLiteral(gTextUriListType)) {
// The desktop or file manager expects for drags of promise-file data
// the text/uri-list flavor set to a temporary file that contains the
// promise-file data.
// We open a stream on the <protocol>:// url here and save the content
// to file:///tmp/dnd_file/<filename> and pass this url
// as text/uri-list flavor.
// check whether transferable contains FilePromiseUrl flavor...
nsresult rv;
nsCOMPtr<nsISupports> data;
rv = item->GetTransferData(kFilePromiseURLMime, getter_AddRefs(data));
// ... if so, create a temporary file and pass its url as
// text/uri-list flavor
if (NS_SUCCEEDED(rv)) {
if (mTempFileUrl.IsEmpty()) {
rv = CreateTempFile(item, aSelectionData);
}
if (NS_SUCCEEDED(rv)) {
LOGDRAGSERVICE(" save tmp file %s", mTempFileUrl.get());
gtk_selection_data_set(aSelectionData, target, 8,
(guchar*)mTempFileUrl.get(),
mTempFileUrl.Length());
// We're done here, data is set.
return;
}
}
// fall back for text/uri-list
LOGDRAGSERVICE(" fall back to plain text/uri-list");
nsAutoCString list;
CreateURIList(mSourceDataItems, list);
gtk_selection_data_set(aSelectionData, target, 8, (guchar*)list.get(),
list.Length());
// We're done here, data is set.
return;
}
// Someone is asking for the special Direct Save Protocol type.
else if (mimeFlavor.EqualsLiteral(gXdndDirectSaveType)) {

View File

@ -100,6 +100,12 @@ class nsDragService final : public nsBaseDragService, public nsIObserver {
GtkSelectionData* aSelectionData);
void SourceDataGetXDND(nsITransferable* aItem, GdkDragContext* aContext,
GtkSelectionData* aSelectionData);
void SourceDataGetUriList(GdkDragContext* aContext,
GtkSelectionData* aSelectionData,
uint32_t aDragItems);
bool SourceDataAppendURLFileItem(nsACString& aURI, nsITransferable* aItem);
bool SourceDataAppendURLItem(nsITransferable* aItem, bool aExternalDrop,
nsACString& aURI);
void SourceBeginDrag(GdkDragContext* aContext);
@ -240,14 +246,13 @@ class nsDragService final : public nsBaseDragService, public nsIObserver {
gboolean DispatchDropEvent();
static uint32_t GetCurrentModifiers();
nsresult CreateTempFile(nsITransferable* aItem,
GtkSelectionData* aSelectionData);
nsresult CreateTempFile(nsITransferable* aItem, nsACString& aURI);
bool RemoveTempFiles();
static gboolean TaskRemoveTempFiles(gpointer data);
// the url of the temporary file that has been created in the current drag
// session
nsCString mTempFileUrl;
nsTArray<nsCString> mTempFileUrls;
// stores all temporary files
nsCOMArray<nsIFile> mTemporaryFiles;
// timer to trigger deletion of temporary files