/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=4 et sw=2 tw=80: */ /* 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 "nsDragService.h" #include "nsArrayUtils.h" #include "nsIObserverService.h" #include "nsWidgetsCID.h" #include "nsWindow.h" #include "nsSystemInfo.h" #include "nsXPCOM.h" #include "nsICookieJarSettings.h" #include "nsISupportsPrimitives.h" #include "nsIIOService.h" #include "nsIFileURL.h" #include "nsNetUtil.h" #include "mozilla/Logging.h" #include "nsTArray.h" #include "nsPrimitiveHelpers.h" #include "prtime.h" #include "prthread.h" #include #include #include #include "nsCRT.h" #include "mozilla/BasicEvents.h" #include "mozilla/Services.h" #include "mozilla/ClearOnShutdown.h" #include "mozilla/PresShell.h" #include "mozilla/ScopeExit.h" #include "mozilla/AutoRestore.h" #include "mozilla/WidgetUtils.h" #include "mozilla/WidgetUtilsGtk.h" #include "mozilla/StaticPrefs_widget.h" #include "GRefPtr.h" #include "nsAppShell.h" #ifdef MOZ_X11 # include "gfxXlibSurface.h" #endif #include "gfxContext.h" #include "nsImageToPixbuf.h" #include "nsPresContext.h" #include "nsIContent.h" #include "mozilla/dom/Document.h" #include "nsViewManager.h" #include "nsIFrame.h" #include "nsGtkUtils.h" #include "nsGtkKeyUtils.h" #include "mozilla/gfx/2D.h" #include "gfxPlatform.h" #include "ScreenHelperGTK.h" #include "nsArrayUtils.h" #include "nsStringStream.h" #include "nsDirectoryService.h" #include "nsDirectoryServiceDefs.h" #include "nsEscape.h" #include "nsString.h" using namespace mozilla; using namespace mozilla::gfx; // The maximum time to wait before temporary files resulting // from drag'n'drop events will be removed in miliseconds. // It's set to 5 min as the file has to be present some time after drop // event to give target application time to get the data. // (A target application can throw a dialog to ask user what to do with // the data and will access the tmp file after user action.) #define NS_DND_TMP_CLEANUP_TIMEOUT (1000 * 60 * 5) #define NS_SYSTEMINFO_CONTRACTID "@mozilla.org/system-info;1" // This sets how opaque the drag image is #define DRAG_IMAGE_ALPHA_LEVEL 0.5 #ifdef MOZ_LOGGING extern mozilla::LazyLogModule gWidgetDragLog; # define LOGDRAGSERVICE(str, ...) \ MOZ_LOG( \ gWidgetDragLog, mozilla::LogLevel::Debug, \ ("[D %d]%s %*s" str, nsDragSession::GetLoopDepth(), \ GetDebugTag().get(), \ nsDragSession::GetLoopDepth() > 1 ? nsDragSession::GetLoopDepth() * 2 \ : 0, \ "", ##__VA_ARGS__)) # define LOGDRAGSERVICESTATIC(str, ...) \ MOZ_LOG(gWidgetDragLog, mozilla::LogLevel::Debug, (str, ##__VA_ARGS__)) #else # define LOGDRAGSERVICE(...) #endif // Helper class to block native events processing. class MOZ_STACK_CLASS AutoSuspendNativeEvents { public: AutoSuspendNativeEvents() { mAppShell = do_GetService(NS_APPSHELL_CID); mAppShell->SuspendNative(); } ~AutoSuspendNativeEvents() { mAppShell->ResumeNative(); } private: nsCOMPtr mAppShell; }; // data used for synthetic periodic motion events sent to the source widget // grabbing real events for the drag. static guint sMotionEventTimerID; static GdkEvent* sMotionEvent; static GUniquePtr TakeMotionEvent() { GUniquePtr event(sMotionEvent); sMotionEvent = nullptr; return event; } static void SetMotionEvent(GUniquePtr aEvent) { TakeMotionEvent(); sMotionEvent = aEvent.release(); } static GtkWidget* sGrabWidget; static constexpr nsLiteralString kDisallowedExportedSchemes[] = { u"about"_ns, u"blob"_ns, u"cached-favicon"_ns, u"chrome"_ns, u"imap"_ns, u"javascript"_ns, u"mailbox"_ns, u"news"_ns, u"page-icon"_ns, u"resource"_ns, u"view-source"_ns, u"moz-extension"_ns, u"moz-page-thumb"_ns, }; // _NETSCAPE_URL is similar to text/uri-list type. // Format is UTF8: URL + "\n" + title. // While text/uri-list tells target application to fetch, copy and store data // from URL, _NETSCAPE_URL suggest to create a link to the target. // Also _NETSCAPE_URL points to only one item while text/uri-list can point to // multiple ones. static const char gMozUrlType[] = "_NETSCAPE_URL"; static const char gMimeListType[] = "application/x-moz-internal-item-list"; static const char gTextUriListType[] = "text/uri-list"; static const char gTextPlainUTF8Type[] = "text/plain;charset=utf-8"; static const char gXdndDirectSaveType[] = "XdndDirectSave0"; static const char gTabDropType[] = "application/x-moz-tabbrowser-tab"; static const char gPortalFile[] = "application/vnd.portal.files"; static const char gPortalFileTransfer[] = "application/vnd.portal.filetransfer"; GdkAtom nsDragSession::sJPEGImageMimeAtom; GdkAtom nsDragSession::sJPGImageMimeAtom; GdkAtom nsDragSession::sPNGImageMimeAtom; GdkAtom nsDragSession::sGIFImageMimeAtom; GdkAtom nsDragSession::sCustomTypesMimeAtom; GdkAtom nsDragSession::sURLMimeAtom; GdkAtom nsDragSession::sRTFMimeAtom; GdkAtom nsDragSession::sTextMimeAtom; GdkAtom nsDragSession::sMozUrlTypeAtom; GdkAtom nsDragSession::sMimeListTypeAtom; GdkAtom nsDragSession::sTextUriListTypeAtom; GdkAtom nsDragSession::sTextPlainUTF8TypeAtom; GdkAtom nsDragSession::sXdndDirectSaveTypeAtom; GdkAtom nsDragSession::sTabDropTypeAtom; GdkAtom nsDragSession::sFileMimeAtom; GdkAtom nsDragSession::sPortalFileAtom; GdkAtom nsDragSession::sPortalFileTransferAtom; GdkAtom nsDragSession::sFilePromiseURLMimeAtom; GdkAtom nsDragSession::sFilePromiseMimeAtom; GdkAtom nsDragSession::sNativeImageMimeAtom; // See https://docs.gtk.org/gtk3/enum.DragResult.html static const char kGtkDragResults[][100]{ "GTK_DRAG_RESULT_SUCCESS", "GTK_DRAG_RESULT_NO_TARGET", "GTK_DRAG_RESULT_USER_CANCELLED", "GTK_DRAG_RESULT_TIMEOUT_EXPIRED", "GTK_DRAG_RESULT_GRAB_BROKEN", "GTK_DRAG_RESULT_ERROR"}; static void invisibleSourceDragBegin(GtkWidget* aWidget, GdkDragContext* aContext, gpointer aData); static void invisibleSourceDragEnd(GtkWidget* aWidget, GdkDragContext* aContext, gpointer aData); static gboolean invisibleSourceDragFailed(GtkWidget* aWidget, GdkDragContext* aContext, gint aResult, gpointer aData); static void invisibleSourceDragDataGet(GtkWidget* aWidget, GdkDragContext* aContext, GtkSelectionData* aSelectionData, guint aInfo, guint32 aTime, gpointer aData); static void UTF16ToNewUTF8(const char16_t* aUTF16, uint32_t aUTF16Len, char** aUTF8, uint32_t* aUTF8Len) { nsDependentSubstring utf16(aUTF16, aUTF16Len); *aUTF8 = ToNewUTF8String(utf16, aUTF8Len); } static nsString UTF8ToNewString(const char* aUTF8, uint32_t aUTF8Len = 0) { nsDependentCSubstring utf8(aUTF8, aUTF8Len ? aUTF8Len : strlen(aUTF8)); nsString ret; uint32_t convertedTextLen = 0; char16_t* convertedText = UTF8ToNewUnicode(utf8, &convertedTextLen); if (!convertedText) { return ret; } convertedTextLen *= 2; // Strip CRLF which might be present in the string. // Not sure where it's added, maybe Gtk? nsLinebreakHelpers::ConvertPlatformToDOMLinebreaks( /* aIsSingleByteChars */ false, (void**)&convertedText, (int32_t*)&convertedTextLen); ret.Adopt(convertedText, convertedTextLen / 2); return ret; } static bool GetFileFromUri(const nsCString& aUri, nsCOMPtr& aFile) { nsresult rv; nsCOMPtr ioService = do_GetIOService(&rv); nsCOMPtr fileURI; if (NS_SUCCEEDED( ioService->NewURI(aUri, nullptr, nullptr, getter_AddRefs(fileURI)))) { nsCOMPtr fileURL = do_QueryInterface(fileURI, &rv); if (NS_SUCCEEDED(rv)) { if (NS_SUCCEEDED(fileURL->GetFile(getter_AddRefs(aFile)))) { return true; } } } LOGDRAG("GetFileFromUri() failed"); return false; } bool DragData::IsImageFlavor() const { return mDataFlavor == nsDragSession::sJPEGImageMimeAtom || mDataFlavor == nsDragSession::sJPGImageMimeAtom || mDataFlavor == nsDragSession::sPNGImageMimeAtom || mDataFlavor == nsDragSession::sGIFImageMimeAtom; } bool DragData::IsFileFlavor() const { return mDataFlavor == nsDragSession::sFileMimeAtom || mDataFlavor == nsDragSession::sPortalFileAtom || mDataFlavor == nsDragSession::sPortalFileTransferAtom; } bool DragData::IsTextFlavor() const { return mDataFlavor == nsDragSession::sTextMimeAtom || mDataFlavor == nsDragSession::sTextPlainUTF8TypeAtom; } bool DragData::IsURIFlavor() const { // We support x-moz-url URL MIME type only return mDataFlavor == nsDragSession::sURLMimeAtom; } int DragData::GetURIsNum() const { int urlNum = 1; if (mDragUris) { urlNum = g_strv_length(mDragUris.get()); } else if (IsURIFlavor()) { urlNum = mUris.Length(); } LOGDRAG("DragData::GetURIsNum() %d", urlNum); return urlNum; } bool DragData::Export(nsITransferable* aTransferable, uint32_t aItemIndex) { GUniquePtr flavorName(gdk_atom_name(mDataFlavor)); LOGDRAG("DragData::Export() MIME %s index %d", flavorName.get(), aItemIndex); if (IsFileFlavor()) { MOZ_ASSERT(mDragUris.get()); char** list = mDragUris.get(); if (aItemIndex >= g_strv_length(list)) { NS_WARNING( nsPrintfCString( "DragData::Export(): Index %d is overflow file list len %d", aItemIndex, g_strv_length(list)) .get()); return false; } bool fileExists = false; nsCOMPtr file; if (GetFileFromUri(nsDependentCString(list[aItemIndex]), file)) { file->Exists(&fileExists); } if (!fileExists) { LOGDRAG(" uri %s not reachable/not found\n", list[aItemIndex]); return false; } LOGDRAG(" export file %s (flavor: %s) as %s", list[aItemIndex], flavorName.get(), kFileMime); aTransferable->SetTransferData(kFileMime, file); return true; } if (IsURIFlavor()) { MOZ_ASSERT(mAsURIData); if (aItemIndex >= mUris.Length()) { NS_WARNING(nsPrintfCString( "DragData::Export(): Index %d is overflow uri list len %d", aItemIndex, (int)mUris.Length()) .get()); return false; } LOGDRAG("%d URI:\n%s", (int)aItemIndex, NS_ConvertUTF16toUTF8(mUris[aItemIndex]).get()); // put it into the transferable. nsCOMPtr genericDataWrapper; nsPrimitiveHelpers::CreatePrimitiveForData( nsAutoCString(kURLMime), mUris[aItemIndex].get(), mUris[aItemIndex].Length() * 2, getter_AddRefs(genericDataWrapper)); return NS_SUCCEEDED( aTransferable->SetTransferData(kURLMime, genericDataWrapper)); } if (IsImageFlavor()) { LOGDRAG(" export image %s", flavorName.get()); nsCOMPtr byteStream; NS_NewByteInputStream(getter_AddRefs(byteStream), mozilla::Span((char*)mDragData.get(), mDragDataLen), NS_ASSIGNMENT_COPY); return NS_SUCCEEDED( aTransferable->SetTransferData(flavorName.get(), byteStream)); } if (IsTextFlavor()) { LOGDRAG(" export text %s", kTextMime); // We get text flavors as UTF8 but we export them as UTF16. if (mData.IsEmpty() && mDragDataLen) { mData = UTF8ToNewString(static_cast(mDragData.get()), mDragDataLen); } // put it into the transferable. nsCOMPtr genericDataWrapper; nsPrimitiveHelpers::CreatePrimitiveForData( nsAutoCString(kTextMime), mData.get(), mData.Length() * 2, getter_AddRefs(genericDataWrapper)); return NS_SUCCEEDED( aTransferable->SetTransferData(kTextMime, genericDataWrapper)); } // We export obtained data directly from Gtk. In such case only // update line endings to DOM format. if (!mDragDataDOMEndings && mDataFlavor != nsDragSession::sCustomTypesMimeAtom) { mDragDataDOMEndings = true; void* tmpData = mDragData.release(); nsLinebreakHelpers::ConvertPlatformToDOMLinebreaks( mDataFlavor == nsDragSession::sRTFMimeAtom, &tmpData, (int32_t*)&mDragDataLen); mDragData.reset(tmpData); } // put it into the transferable. nsCOMPtr genericDataWrapper; nsPrimitiveHelpers::CreatePrimitiveForData( nsDependentCString(flavorName.get()), mDragData.get(), mDragDataLen, getter_AddRefs(genericDataWrapper)); return NS_SUCCEEDED( aTransferable->SetTransferData(flavorName.get(), genericDataWrapper)); } RefPtr DragData::ConvertToMozURL() const { // "text/uri-list" is exported as URI mime type by Gtk, perhaps in UTF8. // We convert it to "text/x-moz-url" which is UTF16 with line breaks. if (mDataFlavor == nsDragSession::sTextUriListTypeAtom) { MOZ_ASSERT(mAsURIData && mDragUris); LOGDRAG("ConvertToMozURL(): text/uri-list => text/x-moz-url"); RefPtr data = new DragData(nsDragSession::sURLMimeAtom); data->mAsURIData = true; int len = g_strv_length(mDragUris.get()); for (int i = 0; i < len; i++) { data->mUris.AppendElement(UTF8ToNewString(mDragUris.get()[i])); } return data; } // MozUrlType (_NETSCAPE_URL) MIME is not registered as URI MIME byt Gtk // is it exports it as plain data. We convert it to "text/x-moz-url" // which is UTF16 with line breaks. if (mDataFlavor == nsDragSession::sMozUrlTypeAtom) { MOZ_ASSERT(mDragData); LOGDRAG("ConvertToMozURL(): _NETSCAPE_URL => text/x-moz-url"); RefPtr data = new DragData(nsDragSession::sURLMimeAtom); data->mAsURIData = true; data->mUris.AppendElement( UTF8ToNewString((const char*)mDragData.get(), mDragDataLen)); return data; } LOGDRAG("ConvertToMozURL(): failed, wrong MIME %s to convert!", GUniquePtr(gdk_atom_name(mDataFlavor)).get()); return nullptr; } RefPtr DragData::ConvertToFile() const { // "text/uri-list" is exported as URI mime type by Gtk, perhaps in UTF8. // We convert it to application/x-moz-file. if (mDataFlavor != nsDragSession::sTextUriListTypeAtom) { return nullptr; } MOZ_ASSERT(mAsURIData && mDragUris); // We can use text/uri-list directly as application/x-moz-file return new DragData(nsDragSession::sFileMimeAtom, g_strdupv(mDragUris.get())); } static int CopyURI(const nsAString& aSourceURL, nsAString& aTargetURL, int aOffset, bool aRequestNewLine) { int32_t uriEnd = aSourceURL.FindChar(u'\n', aOffset); if (uriEnd == aOffset) { return aOffset + 1; } if (uriEnd < 0) { if (aRequestNewLine) { return uriEnd; } // We may miss newline ending on URL title which is correct uriEnd = aSourceURL.Length(); } int32_t newOffset = uriEnd + 1; if (aSourceURL[uriEnd - 1] == u'\r') { uriEnd--; } nsDependentSubstring url(aSourceURL, aOffset, uriEnd - aOffset); if (aRequestNewLine) { url.AppendLiteral("\n"); } aTargetURL.Append(url); return newOffset; } // It holds the URLs of links followed by their titles, // separated by a linebreak. void DragData::ConvertToMozURIList() { if (mDataFlavor != nsDragSession::sURLMimeAtom) { return; } mAsURIData = true; const nsDependentSubstring uris((char16_t*)mDragData.get(), mDragDataLen / 2); LOGDRAG("DragData::ConvertToMozURIList(), data %s", NS_ConvertUTF16toUTF8(uris).get()); int32_t uriBegin = 0; do { nsAutoString uri; // First line contains URL and is terminated by newline if ((uriBegin = CopyURI(uris, uri, uriBegin, /* aRequestNewLine */ true)) < 0) { break; } // Second line is URL title and may be terminated by newline if ((uriBegin = CopyURI(uris, uri, uriBegin, /* aRequestNewLine */ false)) < 0) { break; } LOGDRAG(" URI: %s", NS_ConvertUTF16toUTF8(uri).get()); mUris.AppendElement(uri); } while (uriBegin < (int32_t)uris.Length()); mDragData = nullptr; mDragDataLen = 0; } DragData::DragData(GdkAtom aDataFlavor, gchar** aDragUris) : mDataFlavor(aDataFlavor), mAsURIData(true), mDragUris(aDragUris) {} bool DragData::IsDataValid() const { if (mDragData) { return mDragData.get() && mDragDataLen; } else if (mDragUris) { return !!(mDragUris.get()[0]); } else { return mUris.Length(); } } #ifdef MOZ_LOGGING void DragData::Print() const { if (mDragData) { if (IsTextFlavor()) { nsCString text((char*)mDragData.get(), mDragDataLen); LOGDRAG("DragData() plain data MIME: %s : %s", GUniquePtr(gdk_atom_name(mDataFlavor)).get(), (char*)text.get()); } if (IsURIFlavor()) { nsString text((char16_t*)mDragData.get(), mDragDataLen / 2); LOGDRAG("DragData() plain data MIME: %s : %s", GUniquePtr(gdk_atom_name(mDataFlavor)).get(), NS_ConvertUTF16toUTF8(text).get()); } } else if (mDragUris) { LOGDRAG("DragData() URI MIME %s", GUniquePtr(gdk_atom_name(mDataFlavor)).get()); if (MOZ_LOG_TEST(gWidgetDragLog, mozilla::LogLevel::Debug)) { int i = 0; for (gchar** uri = mDragUris.get(); uri && *uri; uri++, i++) { LOGDRAG("%d URI %s", i, *uri); } } } else if (mUris.Length()) { LOGDRAG("DragData() URI MIME: %s len %d", GUniquePtr(gdk_atom_name(mDataFlavor)).get(), (int)mUris.Length()); for (size_t i = 0; i < mUris.Length(); i++) { LOGDRAG("%d URI:\n%s", (int)i, NS_ConvertUTF16toUTF8(mUris[i]).get()); } } else { LOGDRAG("DragData() MIME %s is missing data", GUniquePtr(gdk_atom_name(mDataFlavor)).get()); } } #endif /* static */ int nsDragSession::sEventLoopDepth = 0; nsDragSession::nsDragSession() { LOGDRAGSERVICE("nsDragSession::nsDragSession()"); // We have to destroy the hidden widget before the event loop stops // running. nsCOMPtr obsServ = mozilla::services::GetObserverService(); obsServ->AddObserver(this, "quit-application", false); // our hidden source widget // Using an offscreen window works around bug 983843. mHiddenWidget = gtk_offscreen_window_new(); // make sure that the widget is realized so that // we can use it as a drag source. gtk_widget_realize(mHiddenWidget); // hook up our internal signals so that we can get some feedback // from our drag source g_signal_connect(mHiddenWidget, "drag_begin", G_CALLBACK(invisibleSourceDragBegin), this); g_signal_connect(mHiddenWidget, "drag_data_get", G_CALLBACK(invisibleSourceDragDataGet), this); g_signal_connect(mHiddenWidget, "drag_end", G_CALLBACK(invisibleSourceDragEnd), this); g_signal_connect(mHiddenWidget, "drag-failed", G_CALLBACK(invisibleSourceDragFailed), this); // set up our logging module mTempFileTimerID = 0; static std::once_flag onceFlag; std::call_once(onceFlag, [] { sJPEGImageMimeAtom = gdk_atom_intern(kJPEGImageMime, FALSE); sJPGImageMimeAtom = gdk_atom_intern(kJPGImageMime, FALSE); sPNGImageMimeAtom = gdk_atom_intern(kPNGImageMime, FALSE); sGIFImageMimeAtom = gdk_atom_intern(kGIFImageMime, FALSE); sCustomTypesMimeAtom = gdk_atom_intern(kCustomTypesMime, FALSE); sURLMimeAtom = gdk_atom_intern(kURLMime, FALSE); sRTFMimeAtom = gdk_atom_intern(kRTFMime, FALSE); sTextMimeAtom = gdk_atom_intern(kTextMime, FALSE); sMozUrlTypeAtom = gdk_atom_intern(gMozUrlType, FALSE); sMimeListTypeAtom = gdk_atom_intern(gMimeListType, FALSE); sTextUriListTypeAtom = gdk_atom_intern(gTextUriListType, FALSE); sTextPlainUTF8TypeAtom = gdk_atom_intern(gTextPlainUTF8Type, FALSE); sXdndDirectSaveTypeAtom = gdk_atom_intern(gXdndDirectSaveType, FALSE); sTabDropTypeAtom = gdk_atom_intern(gTabDropType, FALSE); sFileMimeAtom = gdk_atom_intern(kFileMime, FALSE); sPortalFileAtom = gdk_atom_intern(gPortalFile, FALSE); sPortalFileTransferAtom = gdk_atom_intern(gPortalFileTransfer, FALSE); sFilePromiseURLMimeAtom = gdk_atom_intern(kFilePromiseURLMime, FALSE); sFilePromiseMimeAtom = gdk_atom_intern(kFilePromiseMime, FALSE); sNativeImageMimeAtom = gdk_atom_intern(kNativeImageMime, FALSE); }); } nsDragSession::~nsDragSession() { LOGDRAGSERVICE("nsDragSession::~nsDragSession"); if (mTaskSource) g_source_remove(mTaskSource); if (mTempFileTimerID) { g_source_remove(mTempFileTimerID); RemoveTempFiles(); } } NS_IMPL_ISUPPORTS_INHERITED(nsDragSession, nsBaseDragSession, nsIObserver) mozilla::StaticRefPtr sDragServiceInstance; /* static */ already_AddRefed nsDragService::GetInstance() { if (gfxPlatform::IsHeadless()) { return nullptr; } if (!sDragServiceInstance) { sDragServiceInstance = new nsDragService(); ClearOnShutdown(&sDragServiceInstance); } RefPtr service = sDragServiceInstance.get(); return service.forget(); } already_AddRefed nsDragService::CreateDragSession() { RefPtr session = new nsDragSession(); return session.forget(); } // nsIObserver NS_IMETHODIMP nsDragSession::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) { if (!nsCRT::strcmp(aTopic, "quit-application")) { LOGDRAGSERVICE("nsDragSession::Observe(\"quit-application\")"); if (mHiddenWidget) { gtk_widget_destroy(mHiddenWidget); mHiddenWidget = 0; } } else { MOZ_ASSERT_UNREACHABLE("unexpected topic"); return NS_ERROR_UNEXPECTED; } return NS_OK; } // Support for periodic drag events // http://www.whatwg.org/specs/web-apps/current-work/multipage/dnd.html#drag-and-drop-processing-model // and the Xdnd protocol both recommend that drag events are sent periodically, // but GTK does not normally provide this. // // Here GTK is periodically stimulated by copies of the most recent mouse // motion events so as to send drag position messages to the destination when // appropriate (after it has received a status event from the previous // message). // // (If events were sent only on the destination side then the destination // would have no message to which it could reply with a drag status. Without // sending a drag status to the source, the destination would not be able to // change its feedback re whether it could accept the drop, and so the // source's behavior on drop will not be consistent.) static gboolean DispatchMotionEventCopy(gpointer aData) { // Clear the timer id before OnSourceGrabEventAfter is called during event // dispatch. sMotionEventTimerID = 0; GUniquePtr event = TakeMotionEvent(); // If there is no longer a grab on the widget, then the drag is over and // there is no need to continue drag motion. if (gtk_widget_has_grab(sGrabWidget)) { gtk_propagate_event(sGrabWidget, event.get()); } // Cancel this timer; // We've already started another if the motion event was dispatched. return FALSE; } static void OnSourceGrabEventAfter(GtkWidget* widget, GdkEvent* event, gpointer user_data) { // If there is no longer a grab on the widget, then the drag motion is // over (though the data may not be fetched yet). if (!gtk_widget_has_grab(sGrabWidget)) return; if (event->type == GDK_MOTION_NOTIFY) { SetMotionEvent(GUniquePtr(gdk_event_copy(event))); // Update the cursor position. The last of these recorded gets used for // the eDragEnd event. nsDragService* dragService = static_cast(user_data); nsCOMPtr session; dragService->GetCurrentSession(nullptr, getter_AddRefs(session)); if (session) { gint scale = mozilla::widget::ScreenHelperGTK::GetGTKMonitorScaleFactor(); auto p = LayoutDeviceIntPoint::Round(event->motion.x_root * scale, event->motion.y_root * scale); session->SetDragEndPoint(p.x, p.y); } } else if (sMotionEvent && (event->type == GDK_KEY_PRESS || event->type == GDK_KEY_RELEASE)) { // Update modifier state from key events. sMotionEvent->motion.state = event->key.state; } else { return; } if (sMotionEventTimerID) { g_source_remove(sMotionEventTimerID); } // G_PRIORITY_DEFAULT_IDLE is lower priority than GDK's redraw idle source // and lower than GTK's idle source that sends drag position messages after // motion-notify signals. // // http://www.whatwg.org/specs/web-apps/current-work/multipage/dnd.html#drag-and-drop-processing-model // recommends an interval of 350ms +/- 200ms. sMotionEventTimerID = g_timeout_add_full( G_PRIORITY_DEFAULT_IDLE, 350, DispatchMotionEventCopy, nullptr, nullptr); } static GtkWindow* GetGtkWindow(dom::Document* aDocument) { if (!aDocument) return nullptr; PresShell* presShell = aDocument->GetPresShell(); if (!presShell) { return nullptr; } RefPtr vm = presShell->GetViewManager(); if (!vm) return nullptr; nsCOMPtr widget = vm->GetRootWidget(); if (!widget) return nullptr; GtkWidget* gtkWidget = static_cast(widget.get())->GetGtkWidget(); if (!gtkWidget) return nullptr; GtkWidget* toplevel = nullptr; toplevel = gtk_widget_get_toplevel(gtkWidget); if (!GTK_IS_WINDOW(toplevel)) return nullptr; return GTK_WINDOW(toplevel); } NS_IMETHODIMP nsDragSession::InvokeDragSession( nsIWidget* aWidget, nsINode* aDOMNode, nsIPrincipal* aPrincipal, nsIContentSecurityPolicy* aCsp, nsICookieJarSettings* aCookieJarSettings, nsIArray* aArrayTransferables, uint32_t aActionType, nsContentPolicyType aContentPolicyType = nsIContentPolicy::TYPE_OTHER) { LOGDRAGSERVICE("nsDragSession::InvokeDragSession"); // If the previous source drag has not yet completed, signal handlers need // to be removed from sGrabWidget and dragend needs to be dispatched to // the source node, but we can't call EndDragSession yet because we don't // know whether or not the drag succeeded. if (mSourceNode) return NS_ERROR_NOT_AVAILABLE; return nsBaseDragSession::InvokeDragSession( aWidget, aDOMNode, aPrincipal, aCsp, aCookieJarSettings, aArrayTransferables, aActionType, aContentPolicyType); } // nsBaseDragSession nsresult nsDragSession::InvokeDragSessionImpl( nsIWidget* aWidget, nsIArray* aArrayTransferables, const Maybe& aRegion, uint32_t aActionType) { // make sure that we have an array of transferables to use if (!aArrayTransferables) return NS_ERROR_INVALID_ARG; // set our reference to the transferables. this will also addref // the transferables since we're going to hang onto this beyond the // length of this call mSourceDataItems = aArrayTransferables; LOGDRAGSERVICE("nsDragSession::InvokeDragSessionImpl"); GdkDevice* device = widget::GdkGetPointer(); GdkWindow* originGdkWindow = nullptr; if (widget::GdkIsWaylandDisplay() || widget::IsXWaylandProtocol()) { originGdkWindow = gdk_device_get_window_at_position(device, nullptr, nullptr); // Workaround for https://gitlab.gnome.org/GNOME/gtk/-/issues/6385 // Check we have GdkWindow drag source. if (!originGdkWindow) { NS_WARNING( "nsDragSession::InvokeDragSessionImpl(): Missing origin GdkWindow!"); return NS_ERROR_FAILURE; } } // get the list of items we offer for drags GtkTargetList* sourceList = GetSourceList(); if (!sourceList) return NS_OK; // save our action type GdkDragAction action = GDK_ACTION_DEFAULT; if (aActionType & nsIDragService::DRAGDROP_ACTION_COPY) action = (GdkDragAction)(action | GDK_ACTION_COPY); if (aActionType & nsIDragService::DRAGDROP_ACTION_MOVE) action = (GdkDragAction)(action | GDK_ACTION_MOVE); if (aActionType & nsIDragService::DRAGDROP_ACTION_LINK) action = (GdkDragAction)(action | GDK_ACTION_LINK); GdkEvent* existingEvent = widget::GetLastMousePressEvent(); GdkEvent fakeEvent; if (!existingEvent) { // Create a fake event for the drag so we can pass the time (so to speak). // If we don't do this, then, when the timestamp for the pending button // release event is used for the ungrab, the ungrab can fail due to the // timestamp being _earlier_ than CurrentTime. memset(&fakeEvent, 0, sizeof(GdkEvent)); fakeEvent.type = GDK_BUTTON_PRESS; fakeEvent.button.window = gtk_widget_get_window(mHiddenWidget); fakeEvent.button.time = nsWindow::GetLastUserInputTime(); fakeEvent.button.device = device; } // Put the drag widget in the window group of the source node so that the // gtk_grab_add during gtk_drag_begin is effective. // gtk_window_get_group(nullptr) returns the default window group. GtkWindowGroup* window_group = gtk_window_get_group(GetGtkWindow(mSourceDocument)); gtk_window_group_add_window(window_group, GTK_WINDOW(mHiddenWidget)); // start our drag. GdkDragContext* context = gtk_drag_begin_with_coordinates( mHiddenWidget, sourceList, action, 1, existingEvent ? existingEvent : &fakeEvent, -1, -1); if (originGdkWindow) { mSourceWindow = nsWindow::GetWindow(originGdkWindow); if (mSourceWindow) { mSourceWindow->SetDragSource(context); } } LOGDRAGSERVICE(" GdkDragContext [%p] nsWindow [%p]", context, mSourceWindow.get()); nsresult rv; if (context) { // GTK uses another hidden window for receiving mouse events. sGrabWidget = gtk_window_group_get_current_grab(window_group); if (sGrabWidget) { g_object_ref(sGrabWidget); // Only motion and key events are required but connect to // "event-after" as this is never blocked by other handlers. g_signal_connect(sGrabWidget, "event-after", G_CALLBACK(OnSourceGrabEventAfter), this); } // We don't have a drag end point yet. mEndDragPoint = LayoutDeviceIntPoint(-1, -1); rv = NS_OK; } else { rv = NS_ERROR_FAILURE; } gtk_target_list_unref(sourceList); return rv; } bool nsDragSession::SetAlphaPixmap(SourceSurface* aSurface, GdkDragContext* aContext, int32_t aXOffset, int32_t aYOffset, const LayoutDeviceIntRect& dragRect) { GdkScreen* screen = gtk_widget_get_screen(mHiddenWidget); // Transparent drag icons need, like a lot of transparency-related things, // a compositing X window manager if (!gdk_screen_is_composited(screen)) { return false; } #ifdef cairo_image_surface_create # error "Looks like we're including Mozilla's cairo instead of system cairo" #endif // TODO: grab X11 pixmap or image data instead of expensive readback. cairo_surface_t* surf = cairo_image_surface_create( CAIRO_FORMAT_ARGB32, dragRect.width, dragRect.height); if (!surf) return false; RefPtr dt = gfxPlatform::CreateDrawTargetForData( cairo_image_surface_get_data(surf), nsIntSize(dragRect.width, dragRect.height), cairo_image_surface_get_stride(surf), SurfaceFormat::B8G8R8A8); if (!dt) return false; dt->ClearRect(Rect(0, 0, dragRect.width, dragRect.height)); dt->DrawSurface( aSurface, Rect(0, 0, dragRect.width, dragRect.height), Rect(0, 0, dragRect.width, dragRect.height), DrawSurfaceOptions(), DrawOptions(DRAG_IMAGE_ALPHA_LEVEL, CompositionOp::OP_SOURCE)); cairo_surface_mark_dirty(surf); cairo_surface_set_device_offset(surf, -aXOffset, -aYOffset); // Ensure that the surface is drawn at the correct scale on HiDPI displays. static auto sCairoSurfaceSetDeviceScalePtr = (void (*)(cairo_surface_t*, double, double))dlsym( RTLD_DEFAULT, "cairo_surface_set_device_scale"); if (sCairoSurfaceSetDeviceScalePtr) { gint scale = mozilla::widget::ScreenHelperGTK::GetGTKMonitorScaleFactor(); sCairoSurfaceSetDeviceScalePtr(surf, scale, scale); } gtk_drag_set_icon_surface(aContext, surf); cairo_surface_destroy(surf); return true; } nsIDragSession* nsDragService::StartDragSession(nsISupports* aWidgetProvider) { return nsBaseDragService::StartDragSession(aWidgetProvider); } bool nsDragSession::RemoveTempFiles() { LOGDRAGSERVICE("nsDragSession::RemoveTempFiles"); // We can not delete the temporary files immediately after the // drag has finished, because the target application might have not // copied the temporary file yet. The Qt toolkit does not provide a // way to mark a drop as finished in an asynchronous way, so most // Qt based applications do send the dnd_finished signal before they // have actually accessed the data from the temporary file. // (https://bugreports.qt.io/browse/QTBUG-5441) // // To work also with these applications we collect all temporary // files in mTemporaryFiles array and remove them here in the timer event. auto files = std::move(mTemporaryFiles); for (nsIFile* file : files) { #ifdef MOZ_LOGGING if (MOZ_LOG_TEST(gWidgetDragLog, LogLevel::Debug)) { nsAutoCString path; if (NS_SUCCEEDED(file->GetNativePath(path))) { LOGDRAGSERVICE(" removing %s", path.get()); } } #endif file->Remove(/* recursive = */ true); } MOZ_ASSERT(mTemporaryFiles.IsEmpty()); mTempFileTimerID = 0; // Return false to remove the timer added by g_timeout_add_full(). return false; } /* static */ gboolean nsDragSession::TaskRemoveTempFiles(gpointer data) { // data is a manually addrefed drag session from EndDragSession. // We manually deref it here. RefPtr session = static_cast(data); session.get()->Release(); return session->RemoveTempFiles(); } nsresult nsDragSession::EndDragSessionImpl(bool aDoneDrag, uint32_t aKeyModifiers) { LOGDRAGSERVICE("nsDragSession::EndDragSessionImpl(%p) %d", mTargetDragContext.get(), aDoneDrag); if (sGrabWidget) { g_signal_handlers_disconnect_by_func( sGrabWidget, FuncToGpointer(OnSourceGrabEventAfter), this); g_object_unref(sGrabWidget); sGrabWidget = nullptr; if (sMotionEventTimerID) { g_source_remove(sMotionEventTimerID); sMotionEventTimerID = 0; } if (sMotionEvent) { TakeMotionEvent(); } } // unset our drag action SetDragAction(nsIDragService::DRAGDROP_ACTION_NONE); // start timer to remove temporary files if (mTemporaryFiles.Count() > 0 && !mTempFileTimerID) { LOGDRAGSERVICE(" queue removing of temporary files"); // |this| won't be used after nsDragSession delete because the timer is // removed in the nsDragSession destructor. // Manually addref this and pass to TaskRemoveTempFiles where it is // derefed. AddRef(); mTempFileTimerID = g_timeout_add(NS_DND_TMP_CLEANUP_TIMEOUT, TaskRemoveTempFiles, this); mTempFileUrls.Clear(); } // We're done with the drag context. if (mSourceWindow) { mSourceWindow->SetDragSource(nullptr); mSourceWindow = nullptr; } mTargetDragContextForRemote = nullptr; mTargetWindow = nullptr; mPendingWindow = nullptr; mCachedDragContext = 0; return nsBaseDragSession::EndDragSessionImpl(aDoneDrag, aKeyModifiers); } // nsIDragSession NS_IMETHODIMP nsDragSession::SetCanDrop(bool aCanDrop) { LOGDRAGSERVICE("nsDragSession::SetCanDrop %d", aCanDrop); mCanDrop = aCanDrop; return NS_OK; } NS_IMETHODIMP nsDragSession::GetCanDrop(bool* aCanDrop) { LOGDRAGSERVICE("nsDragSession::GetCanDrop"); *aCanDrop = mCanDrop; return NS_OK; } // Spins event loop, called from JS. // Can lead to another round of drag_motion events. NS_IMETHODIMP nsDragSession::GetNumDropItems(uint32_t* aNumItems) { LOGDRAGSERVICE("nsDragSession::GetNumDropItems"); if (!mTargetWidget) { LOGDRAGSERVICE( "*** warning: GetNumDropItems \ called without a valid target widget!\n"); *aNumItems = 0; return NS_OK; } if (IsTargetContextList()) { if (!mSourceDataItems) { *aNumItems = 0; return NS_OK; } mSourceDataItems->GetLength(aNumItems); LOGDRAGSERVICE("GetNumDropItems(): TargetContextList items %d", *aNumItems); return NS_OK; } // Put text/uri-list first, text/x-moz-url tends to be poorly supported // by third party apps, we got only one file instead of file list // for instance (Bug 1908196). // // We're getting the data to only get number of items here, // actual data will be received at nsDragSession::GetData(). const GdkAtom fileListFlavors[] = {sTextUriListTypeAtom, // text/uri-list sPortalFileAtom, sPortalFileTransferAtom, sURLMimeAtom}; // text/x-moz-url for (auto fileFlavour : fileListFlavors) { RefPtr data = GetDragData(fileFlavour); if (data) { *aNumItems = data->GetURIsNum(); LOGDRAGSERVICE("GetNumDropItems(): Found MIME %s items %d", GUniquePtr(gdk_atom_name(fileFlavour)).get(), *aNumItems); return NS_OK; } } // We're missing any file list MIME, return only one item. *aNumItems = 1; LOGDRAGSERVICE("GetNumDropItems(): no list available"); return NS_OK; } // Spins event loop, called from JS. // Can lead to another round of drag_motion events. NS_IMETHODIMP nsDragSession::GetData(nsITransferable* aTransferable, uint32_t aItemIndex) { LOGDRAGSERVICE("nsDragSession::GetData(), index %d", aItemIndex); // make sure that we have a transferable if (!aTransferable) { return NS_ERROR_INVALID_ARG; } if (!mTargetWidget) { LOGDRAGSERVICE( "*** failed: GetData called without a valid target widget!\n"); return NS_ERROR_FAILURE; } // get flavor list that includes all acceptable flavors (including // ones obtained through conversion). nsTArray flavors; nsresult rv = aTransferable->FlavorsTransferableCanImport(flavors); if (NS_FAILED(rv)) { LOGDRAGSERVICE(" failed to get flavors, quit."); return rv; } // check to see if this is an internal list if (IsTargetContextList()) { LOGDRAGSERVICE(" Process as a list..."); // find a matching flavor for (uint32_t i = 0; i < flavors.Length(); ++i) { nsCString& flavorStr = flavors[i]; LOGDRAGSERVICE(" [%d] flavor is %s\n", i, flavorStr.get()); // get the item with the right index nsCOMPtr item = do_QueryElementAt(mSourceDataItems, aItemIndex); if (!item) continue; nsCOMPtr data; LOGDRAGSERVICE(" trying to get transfer data for %s\n", flavorStr.get()); rv = item->GetTransferData(flavorStr.get(), getter_AddRefs(data)); if (NS_FAILED(rv)) { LOGDRAGSERVICE(" failed.\n"); continue; } rv = aTransferable->SetTransferData(flavorStr.get(), data); if (NS_FAILED(rv)) { LOGDRAGSERVICE(" fail to set transfer data into transferable!\n"); continue; } LOGDRAGSERVICE(" succeeded\n"); // ok, we got the data return NS_OK; } // if we got this far, we failed LOGDRAGSERVICE(" failed to match flavors\n"); return NS_ERROR_FAILURE; } // Now walk down the list of flavors. When we find one that is // actually present, copy out the data into the transferable in that // format. SetTransferData() implicitly handles conversions. for (uint32_t i = 0; i < flavors.Length(); ++i) { nsCString& flavorStr = flavors[i]; GdkAtom requestedFlavor = gdk_atom_intern(flavorStr.get(), FALSE); if (!requestedFlavor) { continue; } LOGDRAGSERVICE(" we're getting data %s\n", flavorStr.get()); RefPtr dragData; // Let's do conversions first. We may be asked for some kind of MIME // type data but we rather try to get something different and // convert to desider MIME type. // We're asked to get text data. Try to get UTF-8 variant first. if (requestedFlavor == sTextMimeAtom) { dragData = GetDragData(sTextPlainUTF8TypeAtom); } // We are looking for text/x-moz-url. That format may be poorly supported, // try first with text/uri-list, and then _NETSCAPE_URL if (requestedFlavor == sURLMimeAtom) { LOGDRAGSERVICE(" conversion %s => %s", gTextUriListType, kURLMime); dragData = GetDragData(sTextUriListTypeAtom); if (dragData) { dragData = dragData->ConvertToMozURL(); mCachedDragData.InsertOrUpdate(dragData->GetFlavor(), dragData); } if (!dragData) { LOGDRAGSERVICE(" conversion %s => %s", gMozUrlType, kURLMime); dragData = GetDragData(sMozUrlTypeAtom); if (dragData) { dragData = dragData->ConvertToMozURL(); if (dragData) { mCachedDragData.InsertOrUpdate(dragData->GetFlavor(), dragData); } } } } // Try to get requested MIME directly if (!dragData) { dragData = GetDragData(requestedFlavor); } // We're asked to get file mime type but we failed. // Try portal variants and text/uri-list conversion. if (!dragData && requestedFlavor == sFileMimeAtom) { // application/vnd.portal.files dragData = GetDragData(sPortalFileAtom); // application/vnd.portal.filetransfer if (!dragData) { dragData = GetDragData(sPortalFileTransferAtom); } if (!dragData) { LOGDRAGSERVICE( " file not found, proceed with conversion %s => %s flavor\n", gTextUriListType, kFileMime); // Conversion text/uri-list => application/x-moz-file dragData = GetDragData(sTextUriListTypeAtom); if (dragData) { dragData = dragData->ConvertToFile(); if (dragData) { mCachedDragData.InsertOrUpdate(dragData->GetFlavor(), dragData); } } } } if (dragData && dragData->Export(aTransferable, aItemIndex)) { // We usually want to also get URL for images so continue if (dragData->IsImageFlavor()) { continue; } return NS_OK; } } return NS_ERROR_FAILURE; } NS_IMETHODIMP nsDragSession::IsDataFlavorSupported(const char* aDataFlavor, bool* _retval) { LOGDRAGSERVICE("nsDragSession::IsDataFlavorSupported(%p) %s", mTargetDragContext.get(), aDataFlavor); if (!_retval) { return NS_ERROR_INVALID_ARG; } // set this to no by default *_retval = false; // check to make sure that we have a drag object set, here if (!mTargetWidget) { LOGDRAGSERVICE( "*** warning: IsDataFlavorSupported called without a valid target " "widget!\n"); return NS_OK; } // check to see if the target context is a list. // if it is, just look in the internal data since we are the source // for it. if (IsTargetContextList()) { LOGDRAGSERVICE(" It's a list"); uint32_t numDragItems = 0; // if we don't have mDataItems we didn't start this drag so it's // an external client trying to fool us. if (!mSourceDataItems) { LOGDRAGSERVICE(" quit"); return NS_OK; } mSourceDataItems->GetLength(&numDragItems); LOGDRAGSERVICE(" drag items %d", numDragItems); for (uint32_t itemIndex = 0; itemIndex < numDragItems; ++itemIndex) { nsCOMPtr currItem = do_QueryElementAt(mSourceDataItems, itemIndex); if (currItem) { nsTArray flavors; currItem->FlavorsTransferableCanExport(flavors); for (uint32_t i = 0; i < flavors.Length(); ++i) { LOGDRAGSERVICE(" checking %s against %s\n", flavors[i].get(), aDataFlavor); if (flavors[i].Equals(aDataFlavor)) { LOGDRAGSERVICE(" found.\n"); *_retval = true; } } } } return NS_OK; } GdkAtom requestedFlavor = gdk_atom_intern(aDataFlavor, FALSE); if (IsDragFlavorAvailable(requestedFlavor)) { LOGDRAGSERVICE(" %s is supported", aDataFlavor); *_retval = true; return NS_OK; } // Check for file/url conversion from uri list if ((requestedFlavor == sURLMimeAtom || requestedFlavor == sFileMimeAtom) && IsDragFlavorAvailable(sTextUriListTypeAtom)) { LOGDRAGSERVICE(" %s supported with conversion from %s", aDataFlavor, gTextUriListType); *_retval = true; return NS_OK; } // check for automatic _NETSCAPE_URL -> text/x-moz-url mapping if (requestedFlavor == sURLMimeAtom && IsDragFlavorAvailable(sMozUrlTypeAtom)) { LOGDRAGSERVICE(" %s supported with conversion from %s", aDataFlavor, gMozUrlType); *_retval = true; return NS_OK; } // If we're asked for kURLMime/kFileMime we can get it from PortalFile // or PortalFileTransfer flavors. if ((requestedFlavor == sURLMimeAtom || requestedFlavor == sFileMimeAtom) && (IsDragFlavorAvailable(sPortalFileAtom) || IsDragFlavorAvailable(sPortalFileTransferAtom))) { LOGDRAGSERVICE(" %s supported with conversion from %s/%s", aDataFlavor, gPortalFile, gPortalFileTransfer); *_retval = true; return NS_OK; } LOGDRAGSERVICE(" %s is not supported", aDataFlavor); return NS_OK; } void nsDragSession::ReplyToDragMotion(GdkDragContext* aDragContext, guint aTime) { LOGDRAGSERVICE("nsDragSession::ReplyToDragMotion(%p) can drop %d", aDragContext, mCanDrop); GdkDragAction action = (GdkDragAction)0; if (mCanDrop) { // notify the dragger if we can drop switch (mDragAction) { case nsIDragService::DRAGDROP_ACTION_COPY: LOGDRAGSERVICE(" set explicit action copy"); action = GDK_ACTION_COPY; break; case nsIDragService::DRAGDROP_ACTION_LINK: LOGDRAGSERVICE(" set explicit action link"); action = GDK_ACTION_LINK; break; case nsIDragService::DRAGDROP_ACTION_NONE: LOGDRAGSERVICE(" set explicit action none"); action = (GdkDragAction)0; break; default: LOGDRAGSERVICE(" set explicit action move"); action = GDK_ACTION_MOVE; break; } } else { LOGDRAGSERVICE(" mCanDrop is false, disable drop"); } // gdk_drag_status() is a kind of red herring here. // It does not control final D&D operation type (copy/move) but controls // drop/no-drop D&D state and default cursor type (copy/move). // Actual D&D operation is determined by mDragAction which is set by // SetDragAction() from UpdateDragAction() or gecko/layout. // State passed to gdk_drag_status() sets default D&D cursor type // which can be switched by key control (CTRL/SHIFT). // If user changes D&D cursor (and D&D operation) we're notified by // gdk_drag_context_get_selected_action() and update mDragAction. // But if we pass mDragAction back to gdk_drag_status() the D&D operation // becames locked and won't be returned when D&D modifiers (CTRL/SHIFT) // are released. // This gdk_drag_context_get_selected_action() -> gdk_drag_status() -> // gdk_drag_context_get_selected_action() cycle happens on Wayland. if (widget::GdkIsWaylandDisplay() && action == GDK_ACTION_COPY) { LOGDRAGSERVICE(" Wayland: switch copy to move"); action = GDK_ACTION_MOVE; } LOGDRAGSERVICE(" gdk_drag_status() action %d", action); gdk_drag_status(aDragContext, action, aTime); } void nsDragSession::SetCachedDragContext(GdkDragContext* aDragContext) { LOGDRAGSERVICE("nsDragSession::SetCachedDragContext(): [drag %p / cached %p]", aDragContext, (void*)mCachedDragContext); // Clear cache data if we're going to D&D with different drag context. uintptr_t recentDragContext = reinterpret_cast(aDragContext); if (recentDragContext && recentDragContext != mCachedDragContext) { LOGDRAGSERVICE(" cache clear, new context %p", (void*)recentDragContext); mCachedDragContext = recentDragContext; mCachedDragData.Clear(); mCachedDragFlavors.Clear(); } } bool nsDragSession::IsTargetContextList(void) { // gMimeListType drags only work for drags within a single process. The // gtk_drag_get_source_widget() function will return nullptr if the source // of the drag is another app, so we use it to check if a gMimeListType // drop will work or not. if (mTargetDragContext && gtk_drag_get_source_widget(mTargetDragContext) == nullptr) { return false; } return IsDragFlavorAvailable(sMimeListTypeAtom); } bool nsDragSession::IsDragFlavorAvailable(GdkAtom aRequestedFlavor) { if (mCachedDragFlavors.IsEmpty()) { for (GList* tmp = gdk_drag_context_list_targets(mTargetDragContext); tmp; tmp = tmp->next) { mCachedDragFlavors.AppendElement(GDK_POINTER_TO_ATOM(tmp->data)); LOGDRAGSERVICE( " adding drag context available flavor %s", GUniquePtr(gdk_atom_name(GDK_POINTER_TO_ATOM(tmp->data))) .get()); } } return mCachedDragFlavors.Contains(aRequestedFlavor); } // Spins event loop, called from eDragTaskMotion handler by // DispatchMotionEvents(). // Can lead to another round of drag_motion events. RefPtr nsDragSession::GetDragData(GdkAtom aRequestedFlavor) { LOGDRAGSERVICE("nsDragSession::GetDragData(%p) requested '%s'\n", mTargetDragContext.get(), GUniquePtr(gdk_atom_name(aRequestedFlavor)).get()); // Return early when requested MIME is not offered by D&D. if (!IsDragFlavorAvailable(aRequestedFlavor)) { LOGDRAGSERVICE(" %s is missing", GUniquePtr(gdk_atom_name(aRequestedFlavor)).get()); return nullptr; } if (!mTargetDragContext) { LOGDRAGSERVICE(" failed, missing mTargetDragContext"); return nullptr; } { auto data = mCachedDragData.MaybeGet(GDK_ATOM_TO_POINTER(aRequestedFlavor)); if (data) { LOGDRAGSERVICE(" MIME %s found in cache, %s", GUniquePtr(gdk_atom_name(aRequestedFlavor)).get(), *data ? "got correctly" : "failed to get"); return *data; } } if (mWaitingForDragDataContext == mTargetDragContext) { LOGDRAGSERVICE(" %s failed to get as we're already waiting to data", GUniquePtr(gdk_atom_name(aRequestedFlavor)).get()); return nullptr; } mWaitingForDragDataContext = mTargetDragContext; // We'll get the data by nsDragSession::TargetDataReceived() gtk_drag_get_data(mTargetWidget, mTargetDragContext, aRequestedFlavor, mTargetTime); LOGDRAGSERVICE(" about to start inner iteration"); gtk_main_iteration(); PRTime entryTime = PR_Now(); int32_t timeout = StaticPrefs::widget_gtk_clipboard_timeout_ms() * 1000; while (mWaitingForDragDataContext && mDoingDrag) { // check the number of iterations LOGDRAGSERVICE(" doing iteration"); if (PR_Now() - entryTime > timeout) { LOGDRAGSERVICE(" failed to get D&D data in time!\n"); break; } gtk_main_iteration(); } // We failed to get all data in time if (mWaitingForDragDataContext) { LOGDRAGSERVICE(" failed to get all data"); } RefPtr data = mCachedDragData.Get(GDK_ATOM_TO_POINTER(aRequestedFlavor)); if (data) { LOGDRAGSERVICE(" %s received", GUniquePtr(gdk_atom_name(aRequestedFlavor)).get()); return data; } LOGDRAGSERVICE(" %s failed to get from system", GUniquePtr(gdk_atom_name(aRequestedFlavor)).get()); return nullptr; } void nsDragSession::TargetDataReceived(GtkWidget* aWidget, GdkDragContext* aContext, gint aX, gint aY, GtkSelectionData* aSelectionData, guint aInfo, guint32 aTime) { MOZ_ASSERT(mWaitingForDragDataContext); GdkAtom target = gtk_selection_data_get_target(aSelectionData); LOGDRAGSERVICE("nsDragSession::TargetDataReceived(%p) MIME %s ", aContext, GUniquePtr(gdk_atom_name(target)).get()); if (mWaitingForDragDataContext != aContext) { LOGDRAGSERVICE(" quit - wrong drag context!"); return; } mWaitingForDragDataContext = nullptr; RefPtr dragData; auto saveData = MakeScopeExit([&] { if (dragData && !dragData->IsDataValid()) { dragData = nullptr; } if (!dragData) { LOGDRAGSERVICE(" failed to get data, MIME %s", GUniquePtr(gdk_atom_name(target)).get()); } // We set cache even for empty received data. // It saves time if we're asked for the same data type // again. mCachedDragData.InsertOrUpdate(target, dragData); }); if (target == sPortalFileAtom || target == sPortalFileTransferAtom) { const guchar* data = gtk_selection_data_get_data(aSelectionData); if (!data || data[0] == '\0') { LOGDRAGSERVICE( "nsDragSession::TargetDataReceived() failed to get file portal data " "(%s)", GUniquePtr(gdk_atom_name(target)).get()); return; } // A workaround for https://gitlab.gnome.org/GNOME/gtk/-/issues/6563 // // For the vnd.portal.filetransfer and vnd.portal.files we receive numeric // id when it's a local file. The numeric id is then used by // gtk_selection_data_get_uris implementation to get the actual file // available in the flatpak environment. // // However due to GTK implementation also for example the uris like https // are also provided by the vnd.portal.filetransfer target. In this case // the call gtk_selection_data_get_uris fails. This is a bug in the gtk. // To workaround it we try to create the valid uri and only if we fail // we try to use the gtk_selection_data_get_uris. We ignore the valid uris // for the vnd.portal.file* targets. nsCOMPtr sourceURI; nsresult rv = NS_NewURI(getter_AddRefs(sourceURI), (const gchar*)data, nullptr); if (NS_SUCCEEDED(rv)) { LOGDRAGSERVICE( " TargetDataReceived(): got valid uri for MIME %s - this is bug " "in GTK - expected numeric value for portal, got %s\n", GUniquePtr(gdk_atom_name(target)).get(), data); return; } dragData = new DragData(target, gtk_selection_data_get_uris(aSelectionData)); LOGDRAGSERVICE(" TargetDataReceived(): FILE PORTAL data, MIME %s", GUniquePtr(gdk_atom_name(target)).get()); } else if (target == sTextUriListTypeAtom) { dragData = new DragData(target, gtk_selection_data_get_uris(aSelectionData)); LOGDRAGSERVICE(" TargetDataReceived(): URI data, MIME %s", GUniquePtr(gdk_atom_name(target)).get()); } else { const guchar* data = gtk_selection_data_get_data(aSelectionData); gint len = gtk_selection_data_get_length(aSelectionData); if (len < 0 && !data) { LOGDRAGSERVICE(" TargetDataReceived() failed"); return; } dragData = new DragData(target, data, len); LOGDRAGSERVICE(" TargetDataReceived(): plain data, MIME %s len = %d", GUniquePtr(gdk_atom_name(target)).get(), len); } #if MOZ_LOGGING if (dragData) { dragData->Print(); } #endif } static void TargetArrayAddTarget(nsTArray& aTargetArray, const char* aTarget) { GtkTargetEntry* target = (GtkTargetEntry*)g_malloc(sizeof(GtkTargetEntry)); target->target = g_strdup(aTarget); target->flags = 0; aTargetArray.AppendElement(target); LOGDRAGSERVICESTATIC("adding target %s\n", aTarget); } static bool CanExportAsURLTarget(const char16_t* aURLData, uint32_t aURLLen) { for (const nsLiteralString& disallowed : kDisallowedExportedSchemes) { auto len = disallowed.AsString().Length(); if (len < aURLLen) { if (!memcmp(disallowed.get(), aURLData, /* len is char16_t char count */ len * 2)) { LOGDRAGSERVICESTATIC("rejected URL scheme %s\n", NS_ConvertUTF16toUTF8(disallowed).get()); return false; } } } return true; } GtkTargetList* nsDragSession::GetSourceList(void) { if (!mSourceDataItems) { return nullptr; } nsTArray targetArray; GtkTargetEntry* targets; GtkTargetList* targetList = 0; uint32_t targetCount = 0; unsigned int numDragItems = 0; mSourceDataItems->GetLength(&numDragItems); LOGDRAGSERVICE(" numDragItems = %d", numDragItems); // Check to see if we're dragging > 1 item. if (numDragItems > 1) { // as the Xdnd protocol only supports a single item (or is it just // gtk's implementation?), we don't advertise all flavours listed // in the nsITransferable. // the application/x-moz-internal-item-list format, which preserves // all information for drags within the same mozilla instance. TargetArrayAddTarget(targetArray, gMimeListType); // check what flavours are supported so we can decide what other // targets to advertise. nsCOMPtr currItem = do_QueryElementAt(mSourceDataItems, 0); if (currItem) { nsTArray flavors; currItem->FlavorsTransferableCanExport(flavors); for (uint32_t i = 0; i < flavors.Length(); ++i) { // check if text/x-moz-url is supported. // If so, advertise // text/uri-list. if (flavors[i].EqualsLiteral(kURLMime)) { TargetArrayAddTarget(targetArray, gTextUriListType); break; } } } // if item is a transferable } else if (numDragItems == 1) { nsCOMPtr currItem = do_QueryElementAt(mSourceDataItems, 0); if (currItem) { nsTArray flavors; currItem->FlavorsTransferableCanExport(flavors); for (uint32_t i = 0; i < flavors.Length(); ++i) { nsCString& flavorStr = flavors[i]; GdkAtom requestedFlavor = gdk_atom_intern(flavorStr.get(), FALSE); if (!requestedFlavor) { continue; } TargetArrayAddTarget(targetArray, flavorStr.get()); // If there is a file, add the text/uri-list type. if (requestedFlavor == sFileMimeAtom) { TargetArrayAddTarget(targetArray, gTextUriListType); } // Check to see if this is text/plain. else if (requestedFlavor == sTextMimeAtom) { TargetArrayAddTarget(targetArray, gTextPlainUTF8Type); } // Check to see if this is the x-moz-url type. // If it is, add _NETSCAPE_URL // this is a type used by everybody. else if (requestedFlavor == sURLMimeAtom) { nsCOMPtr data; if (NS_SUCCEEDED(currItem->GetTransferData(flavorStr.get(), getter_AddRefs(data)))) { void* tmpData = nullptr; uint32_t tmpDataLen = 0; nsPrimitiveHelpers::CreateDataFromPrimitive( nsDependentCString(flavorStr.get()), data, &tmpData, &tmpDataLen); if (tmpData) { if (CanExportAsURLTarget(reinterpret_cast(tmpData), tmpDataLen / 2)) { TargetArrayAddTarget(targetArray, gMozUrlType); } free(tmpData); } } } // check if application/x-moz-file-promise url is supported. // If so, advertise text/uri-list. else if (requestedFlavor == sFilePromiseURLMimeAtom) { TargetArrayAddTarget(targetArray, gTextUriListType); } // XdndDirectSave, use on X.org only. else if (widget::GdkIsX11Display() && !widget::IsXWaylandProtocol() && requestedFlavor == sFilePromiseMimeAtom) { TargetArrayAddTarget(targetArray, gXdndDirectSaveType); } // kNativeImageMime else if (requestedFlavor == sNativeImageMimeAtom) { TargetArrayAddTarget(targetArray, kPNGImageMime); TargetArrayAddTarget(targetArray, kJPEGImageMime); TargetArrayAddTarget(targetArray, kJPGImageMime); TargetArrayAddTarget(targetArray, kGIFImageMime); } } } } // get all the elements that we created. targetCount = targetArray.Length(); if (targetCount) { // allocate space to create the list of valid targets targets = (GtkTargetEntry*)g_malloc(sizeof(GtkTargetEntry) * targetCount); uint32_t targetIndex; for (targetIndex = 0; targetIndex < targetCount; ++targetIndex) { GtkTargetEntry* disEntry = targetArray.ElementAt(targetIndex); // this is a string reference but it will be freed later. targets[targetIndex].target = disEntry->target; targets[targetIndex].flags = disEntry->flags; targets[targetIndex].info = 0; } targetList = gtk_target_list_new(targets, targetCount); // clean up the target list for (uint32_t cleanIndex = 0; cleanIndex < targetCount; ++cleanIndex) { GtkTargetEntry* thisTarget = targetArray.ElementAt(cleanIndex); g_free(thisTarget->target); g_free(thisTarget); } g_free(targets); } else { // We need to create a dummy target list to be able initialize dnd. targetList = gtk_target_list_new(nullptr, 0); } return targetList; } void nsDragSession::SourceEndDragSession(GdkDragContext* aContext, gint aResult) { LOGDRAGSERVICE("SourceEndDragSession(%p) result %s\n", aContext, kGtkDragResults[aResult]); // this just releases the list of data items that we provide mSourceDataItems = nullptr; // Remove this property, if it exists, to satisfy the Direct Save Protocol. GdkAtom property = sXdndDirectSaveTypeAtom; gdk_property_delete(gdk_drag_context_get_source_window(aContext), property); if (!mDoingDrag || mScheduledTask == eDragTaskSourceEnd) // EndDragSession() was already called on drop // or SourceEndDragSession on drag-failed return; if (mEndDragPoint.x < 0) { // We don't have a drag end point, so guess gint x, y; GdkDisplay* display = gdk_display_get_default(); GdkScreen* screen = gdk_display_get_default_screen(display); GtkWindow* window = GetGtkWindow(mSourceDocument); GdkWindow* gdkWindow = window ? gtk_widget_get_window(GTK_WIDGET(window)) : gdk_screen_get_root_window(screen); if (!gdkWindow) { return; } gdk_window_get_device_position( gdkWindow, gdk_drag_context_get_device(aContext), &x, &y, nullptr); gint scale = gdk_window_get_scale_factor(gdkWindow); SetDragEndPoint(x * scale, y * scale); LOGDRAGSERVICE(" guess drag end point %d %d\n", x * scale, y * scale); } // Either the drag was aborted or the drop occurred outside the app. // The dropEffect of mDataTransfer is not updated for motion outside the // app, but is needed for the dragend event, so set it now. uint32_t dropEffect; if (aResult == GTK_DRAG_RESULT_SUCCESS) { LOGDRAGSERVICE(" drop is accepted"); // With GTK+ versions 2.10.x and prior the drag may have been // cancelled (but no drag-failed signal would have been sent). // aContext->dest_window will be non-nullptr only if the drop was // sent. GdkDragAction action = gdk_drag_context_get_dest_window(aContext) ? gdk_drag_context_get_actions(aContext) : (GdkDragAction)0; // Only one bit of action should be set, but, just in case someone // does something funny, erring away from MOVE, and not recording // unusual action combinations as NONE. if (!action) { LOGDRAGSERVICE(" drop action is none"); dropEffect = nsIDragService::DRAGDROP_ACTION_NONE; } else if (action & GDK_ACTION_COPY) { LOGDRAGSERVICE(" drop action is copy"); dropEffect = nsIDragService::DRAGDROP_ACTION_COPY; } else if (action & GDK_ACTION_LINK) { LOGDRAGSERVICE(" drop action is link"); dropEffect = nsIDragService::DRAGDROP_ACTION_LINK; } else if (action & GDK_ACTION_MOVE) { LOGDRAGSERVICE(" drop action is move"); dropEffect = nsIDragService::DRAGDROP_ACTION_MOVE; } else { LOGDRAGSERVICE(" drop action is copy"); dropEffect = nsIDragService::DRAGDROP_ACTION_COPY; } } else { LOGDRAGSERVICE(" drop action is none"); dropEffect = nsIDragService::DRAGDROP_ACTION_NONE; if (aResult != GTK_DRAG_RESULT_NO_TARGET) { LOGDRAGSERVICE(" drop is user chancelled\n"); mUserCancelled = true; } } if (mDataTransfer) { mDataTransfer->SetDropEffectInt(dropEffect); } // Schedule the appropriate drag end dom events. Schedule(eDragTaskSourceEnd, mTargetWindow, nullptr, LayoutDeviceIntPoint(), 0); } static nsresult GetDownloadDetails(nsITransferable* aTransferable, nsIURI** aSourceURI, nsAString& aFilename) { *aSourceURI = nullptr; MOZ_ASSERT(aTransferable != nullptr, "aTransferable must not be null"); // get the URI from the kFilePromiseURLMime flavor nsCOMPtr urlPrimitive; nsresult rv = aTransferable->GetTransferData(kFilePromiseURLMime, getter_AddRefs(urlPrimitive)); if (NS_FAILED(rv)) { return NS_ERROR_FAILURE; } nsCOMPtr srcUrlPrimitive = do_QueryInterface(urlPrimitive); if (!srcUrlPrimitive) { return NS_ERROR_FAILURE; } nsAutoString srcUri; srcUrlPrimitive->GetData(srcUri); if (srcUri.IsEmpty()) { return NS_ERROR_FAILURE; } nsCOMPtr sourceURI; NS_NewURI(getter_AddRefs(sourceURI), srcUri); nsAutoString srcFileName; nsCOMPtr fileNamePrimitive; rv = aTransferable->GetTransferData(kFilePromiseDestFilename, getter_AddRefs(fileNamePrimitive)); if (NS_FAILED(rv)) { return NS_ERROR_FAILURE; } nsCOMPtr srcFileNamePrimitive = do_QueryInterface(fileNamePrimitive); if (srcFileNamePrimitive) { srcFileNamePrimitive->GetData(srcFileName); } else { nsCOMPtr sourceURL = do_QueryInterface(sourceURI); if (!sourceURL) { return NS_ERROR_FAILURE; } nsAutoCString urlFileName; sourceURL->GetFileName(urlFileName); NS_UnescapeURL(urlFileName); CopyUTF8toUTF16(urlFileName, srcFileName); } if (srcFileName.IsEmpty()) { return NS_ERROR_FAILURE; } sourceURI.swap(*aSourceURI); aFilename = srcFileName; return NS_OK; } // See nsContentAreaDragDropDataProvider::GetFlavorData() for reference. nsresult nsDragSession::CreateTempFile(nsITransferable* aItem, nsACString& aURI) { LOGDRAGSERVICE("nsDragSession::CreateTempFile()"); nsCOMPtr 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; } nsCOMPtr inputStream; nsCOMPtr channel; // extract the file name and source uri of the promise-file data nsAutoString wideFileName; nsCOMPtr sourceURI; rv = GetDownloadDetails(aItem, getter_AddRefs(sourceURI), wideFileName); if (NS_FAILED(rv)) { LOGDRAGSERVICE( " Failed to extract file name and source uri from download url"); 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 principal = aItem->GetDataPrincipal(); nsContentPolicyType contentPolicyType = aItem->GetContentPolicyType(); nsCOMPtr cookieJarSettings = aItem->GetCookieJarSettings(); rv = NS_NewChannel(getter_AddRefs(channel), sourceURI, principal, nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, contentPolicyType, cookieJarSettings); if (NS_FAILED(rv)) { LOGDRAGSERVICE(" Failed to create new channel for source uri"); return rv; } rv = channel->Open(getter_AddRefs(inputStream)); if (NS_FAILED(rv)) { LOGDRAGSERVICE(" Failed to open channel for source uri"); return rv; } // build the file:///tmp/dnd_file URL tmpDir->Append(NS_LITERAL_STRING_FROM_CSTRING("dnd_file")); rv = tmpDir->CreateUnique(nsIFile::DIRECTORY_TYPE, 0700); if (NS_FAILED(rv)) { LOGDRAGSERVICE(" Failed create tmp dir"); return rv; } // store a copy of that temporary directory so we can // clean them up when nsDragSession is destructed nsCOMPtr tempFile; tmpDir->Clone(getter_AddRefs(tempFile)); mTemporaryFiles.AppendObject(tempFile); if (mTempFileTimerID) { g_source_remove(mTempFileTimerID); mTempFileTimerID = 0; } // extend file:///tmp/dnd_file/ URL tmpDir->Append(wideFileName); nsCOMPtr outputStream; rv = NS_NewLocalFileOutputStream(getter_AddRefs(outputStream), tmpDir); if (NS_FAILED(rv)) { LOGDRAGSERVICE(" Failed to open output stream for temporary file"); return rv; } char buffer[8192]; uint32_t readCount = 0; uint32_t writeCount = 0; while (1) { rv = inputStream->Read(buffer, sizeof(buffer), &readCount); if (NS_FAILED(rv)) { LOGDRAGSERVICE(" Failed to read data from source uri"); return rv; } if (readCount == 0) break; rv = outputStream->Write(buffer, readCount, &writeCount); if (NS_FAILED(rv)) { LOGDRAGSERVICE(" Failed to write data to temporary file"); return rv; } } inputStream->Close(); rv = outputStream->Close(); if (NS_FAILED(rv)) { LOGDRAGSERVICE(" Failed to write data to temporary file"); return rv; } nsCOMPtr uri; rv = NS_NewFileURI(getter_AddRefs(uri), tmpDir); if (NS_FAILED(rv)) { LOGDRAGSERVICE(" Failed to get file URI"); return rv; } nsCOMPtr 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; } // store url of temporary file mTempFileUrls.AppendElement()->Assign(aURI); LOGDRAGSERVICE(" storing tmp file as %s", PromiseFlatCString(aURI).get()); return NS_OK; } bool nsDragSession::SourceDataAppendURLFileItem(nsACString& aURI, nsITransferable* aItem) { // If there is a file available, create a URI from the file. nsCOMPtr data; nsresult rv = aItem->GetTransferData(kFileMime, getter_AddRefs(data)); NS_ENSURE_SUCCESS(rv, false); if (nsCOMPtr file = do_QueryInterface(data)) { nsCOMPtr fileURI; NS_NewFileURI(getter_AddRefs(fileURI), file); if (fileURI) { fileURI->GetSpec(aURI); return true; } } return false; } bool nsDragSession::SourceDataAppendURLItem(nsITransferable* aItem, bool aExternalDrop, nsACString& aURI) { nsCOMPtr data; nsresult rv = aItem->GetTransferData(kURLMime, getter_AddRefs(data)); if (NS_FAILED(rv)) { return SourceDataAppendURLFileItem(aURI, aItem); } nsCOMPtr 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 :// url here and save the content // to file:///tmp/dnd_file/ and pass this url // as text/uri-list flavor. // check whether transferable contains FilePromiseUrl flavor... nsCOMPtr 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 nsDragSession::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("nsDragSession::SourceDataGetUriLists() len %d external %d", aDragItems, isExternalDrop); // Disable processing of native events until we store all files to /tmp. // Otherwise user can quit drop before we have all files saved // and that cancels whole D&D. AutoSuspendNativeEvents suspend; nsAutoCString uriList; for (uint32_t i = 0; i < aDragItems; i++) { nsCOMPtr 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()); } bool nsDragSession::SourceDataGetImage(nsITransferable* aItem, GtkSelectionData* aSelectionData) { LOGDRAGSERVICE("nsDragSession::SourceDataGetImage()"); nsresult rv; nsCOMPtr data; rv = aItem->GetTransferData(kNativeImageMime, getter_AddRefs(data)); NS_ENSURE_SUCCESS(rv, false); LOGDRAGSERVICE(" posting image\n"); nsCOMPtr image = do_QueryInterface(data); if (!image) { LOGDRAGSERVICE(" do_QueryInterface failed\n"); return false; } RefPtr pixbuf = nsImageToPixbuf::ImageToPixbuf(image); if (!pixbuf) { LOGDRAGSERVICE(" ImageToPixbuf failed\n"); return false; } gtk_selection_data_set_pixbuf(aSelectionData, pixbuf); LOGDRAGSERVICE(" image data set\n"); return true; } bool nsDragSession::SourceDataGetXDND(nsITransferable* aItem, GdkDragContext* aContext, GtkSelectionData* aSelectionData) { LOGDRAGSERVICE("nsDragSession::SourceDataGetXDND"); // Indicate failure by default. GdkAtom target = gtk_selection_data_get_target(aSelectionData); gtk_selection_data_set(aSelectionData, target, 8, (guchar*)"E", 1); GdkWindow* srcWindow = gdk_drag_context_get_source_window(aContext); if (!srcWindow) { LOGDRAGSERVICE(" failed to get source GdkWindow!"); return false; } // Ensure null termination. nsAutoCString data; { GUniquePtr gdata; gint length = 0; if (!gdk_property_get(srcWindow, sXdndDirectSaveTypeAtom, sTextMimeAtom, 0, INT32_MAX, FALSE, nullptr, nullptr, &length, getter_Transfers(gdata))) { LOGDRAGSERVICE(" failed to get gXdndDirectSaveType GdkWindow property."); return false; } data.Assign(nsDependentCSubstring((const char*)gdata.get(), length)); } GUniquePtr hostname; GUniquePtr fullpath( g_filename_from_uri(data.get(), getter_Transfers(hostname), nullptr)); if (!fullpath) { LOGDRAGSERVICE(" failed to get file from uri."); return false; } // If there is no hostname in the URI, NULL will be stored. // We should not accept uris with from a different host. if (hostname) { nsCOMPtr infoService = do_GetService(NS_SYSTEMINFO_CONTRACTID); if (!infoService) { return false; } nsAutoCString host; if (NS_SUCCEEDED(infoService->GetPropertyAsACString(u"host"_ns, host))) { if (!host.Equals(hostname.get())) { LOGDRAGSERVICE(" ignored drag because of different host."); // Special error code "F" for this case. gtk_selection_data_set(aSelectionData, target, 8, (guchar*)"F", 1); return true; } } } LOGDRAGSERVICE(" XdndDirectSave filepath is %s", fullpath.get()); nsCOMPtr file; if (NS_FAILED(NS_NewNativeLocalFile(nsDependentCString(fullpath.get()), getter_AddRefs(file)))) { LOGDRAGSERVICE(" failed to get local file"); return false; } // We have to split the path into a directory and filename, // because our internal file-promise API is based on these. nsCOMPtr directory; file->GetParent(getter_AddRefs(directory)); aItem->SetTransferData(kFilePromiseDirectoryMime, directory); nsCOMPtr filenamePrimitive = do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID); if (!filenamePrimitive) { return false; } nsAutoString leafName; file->GetLeafName(leafName); filenamePrimitive->SetData(leafName); aItem->SetTransferData(kFilePromiseDestFilename, filenamePrimitive); nsCOMPtr promiseData; nsresult rv = aItem->GetTransferData(kFilePromiseMime, getter_AddRefs(promiseData)); NS_ENSURE_SUCCESS(rv, false); // Indicate success. gtk_selection_data_set(aSelectionData, target, 8, (guchar*)"S", 1); return true; } bool nsDragSession::SourceDataGetText(nsITransferable* aItem, const nsACString& aMIMEType, bool aNeedToDoConversionToPlainText, GtkSelectionData* aSelectionData) { LOGDRAGSERVICE("nsDragSession::SourceDataGetPlain()"); nsresult rv; nsCOMPtr data; rv = aItem->GetTransferData(PromiseFlatCString(aMIMEType).get(), getter_AddRefs(data)); NS_ENSURE_SUCCESS(rv, false); void* tmpData = nullptr; uint32_t tmpDataLen = 0; nsPrimitiveHelpers::CreateDataFromPrimitive(aMIMEType, data, &tmpData, &tmpDataLen); // if required, do the extra work to convert unicode to plain // text and replace the output values with the plain text. if (aNeedToDoConversionToPlainText) { char* plainTextData = nullptr; char16_t* castedUnicode = reinterpret_cast(tmpData); uint32_t plainTextLen = 0; UTF16ToNewUTF8(castedUnicode, tmpDataLen / 2, &plainTextData, &plainTextLen); if (tmpData) { // this was not allocated using glib free(tmpData); tmpData = plainTextData; tmpDataLen = plainTextLen; } } if (tmpData) { // this copies the data GdkAtom target = gtk_selection_data_get_target(aSelectionData); gtk_selection_data_set(aSelectionData, target, 8, (guchar*)tmpData, tmpDataLen); // this wasn't allocated with glib free(tmpData); } return true; } // We're asked to get data from mSourceDataItems and pass it to // GtkSelectionData (Gtk D&D interface). // We need to check mSourceDataItems data type and try to convert it // to data type accepted by Gtk. void nsDragSession::SourceDataGet(GtkWidget* aWidget, GdkDragContext* aContext, GtkSelectionData* aSelectionData, guint32 aTime) { GdkAtom requestedFlavor = gtk_selection_data_get_target(aSelectionData); LOGDRAGSERVICE("nsDragSession::SourceDataGet(%p) MIME %s", aContext, GUniquePtr(gdk_atom_name(requestedFlavor)).get()); // check to make sure that we have data items to return. if (!mSourceDataItems) { LOGDRAGSERVICE(" Failed to get our data items\n"); return; } uint32_t dragItems; mSourceDataItems->GetLength(&dragItems); LOGDRAGSERVICE(" source data items %d", dragItems); if (requestedFlavor == sTextUriListTypeAtom) { SourceDataGetUriList(aContext, aSelectionData, dragItems); return; } #ifdef MOZ_LOGGING 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, GUniquePtr(gdk_atom_name(requestedFlavor)).get()); } #endif nsCOMPtr item = do_QueryElementAt(mSourceDataItems, 0); if (!item) { LOGDRAGSERVICE(" Failed to get SourceDataItems!"); return; } if (requestedFlavor == sTextMimeAtom || requestedFlavor == sTextPlainUTF8TypeAtom) { if (!SourceDataGetText(item, nsDependentCString(kTextMime), /* aNeedToDoConversionToPlainText */ true, aSelectionData)) { LOGDRAGSERVICE( " Failed to send sTextMimeAtom/sTextPlainUTF8TypeAtom data!"); } // no fallback for text mime types return; } // Someone is asking for the special Direct Save Protocol type. else if (requestedFlavor == sXdndDirectSaveTypeAtom) { if (!SourceDataGetXDND(item, aContext, aSelectionData)) { LOGDRAGSERVICE(" Failed to send sXdndDirectSaveTypeAtom data!"); } // no fallback for XDND mime types return; } else if (requestedFlavor == sPNGImageMimeAtom || requestedFlavor == sJPEGImageMimeAtom || requestedFlavor == sJPGImageMimeAtom || requestedFlavor == sGIFImageMimeAtom) { // no fallback for image mime types if (!SourceDataGetImage(item, aSelectionData)) { LOGDRAGSERVICE(" Failed to send image data!"); } return; } else if (requestedFlavor == sMozUrlTypeAtom) { // Someone was asking for _NETSCAPE_URL. We need to get it from // transferable as x-moz-url and convert it to plain text. // No need to check if (SourceDataGetText(item, nsDependentCString(kURLMime), /* aNeedToDoConversionToPlainText */ true, aSelectionData)) { LOGDRAGSERVICE(" Failed to send kURLMime data!"); return; } } // Just try to get and set whatever we're asked for. GUniquePtr flavorName(gdk_atom_name(requestedFlavor)); if (!SourceDataGetText(item, nsDependentCString(flavorName.get()), /* aNeedToDoConversionToPlainText */ false, aSelectionData)) { LOGDRAGSERVICE(" Failed to send %s data!", nsDependentCString(flavorName.get()).get()); } } void nsDragSession::SourceBeginDrag(GdkDragContext* aContext) { LOGDRAGSERVICE("nsDragSession::SourceBeginDrag(%p)\n", aContext); nsCOMPtr transferable = do_QueryElementAt(mSourceDataItems, 0); if (!transferable) { LOGDRAGSERVICE(" missing transferable!"); return; } nsTArray flavors; nsresult rv = transferable->FlavorsTransferableCanImport(flavors); if (NS_FAILED(rv)) { LOGDRAGSERVICE(" FlavorsTransferableCanImport failed!"); return; } for (uint32_t i = 0; i < flavors.Length(); ++i) { if (flavors[i].EqualsLiteral(kFilePromiseDestFilename)) { nsCOMPtr data; rv = transferable->GetTransferData(kFilePromiseDestFilename, getter_AddRefs(data)); if (NS_FAILED(rv)) { LOGDRAGSERVICE(" transferable doesn't contain '%s", kFilePromiseDestFilename); return; } nsCOMPtr fileName = do_QueryInterface(data); if (!fileName) { LOGDRAGSERVICE(" failed to get file name"); return; } nsAutoString fileNameStr; fileName->GetData(fileNameStr); nsCString fileNameCStr; CopyUTF16toUTF8(fileNameStr, fileNameCStr); gdk_property_change( gdk_drag_context_get_source_window(aContext), sXdndDirectSaveTypeAtom, sTextMimeAtom, 8, GDK_PROP_MODE_REPLACE, (const guchar*)fileNameCStr.get(), fileNameCStr.Length()); break; } } } void nsDragSession::SetDragIcon(GdkDragContext* aContext) { if (!mHasImage && !mSelection) return; LOGDRAGSERVICE("nsDragSession::SetDragIcon(%p)", aContext); LayoutDeviceIntRect dragRect; nsPresContext* pc; RefPtr surface; DrawDrag(mSourceNode, mRegion, mScreenPosition, &dragRect, &surface, &pc); if (!pc) { LOGDRAGSERVICE(" PresContext is missing!"); return; } const auto screenPoint = LayoutDeviceIntPoint::Round(mScreenPosition * pc->CSSToDevPixelScale()); int32_t offsetX = screenPoint.x - dragRect.x; int32_t offsetY = screenPoint.y - dragRect.y; // If a popup is set as the drag image, use its widget. Otherwise, use // the surface that DrawDrag created. // // XXX: Disable drag popups on GTK 3.19.4 and above: see bug 1264454. // Fix this once a new GTK version ships that does not destroy our // widget in gtk_drag_set_icon_widget. // This is fixed in GTK 3.24 // by // https://gitlab.gnome.org/GNOME/gtk/-/commit/c27c4e2048acb630feb24c31288f802345e99f4c bool gtk_drag_set_icon_widget_is_working = gtk_check_version(3, 19, 4) != nullptr || gtk_check_version(3, 24, 0) == nullptr; if (mDragPopup && gtk_drag_set_icon_widget_is_working) { GtkWidget* gtkWidget = nullptr; nsIFrame* frame = mDragPopup->GetPrimaryFrame(); if (frame) { // DrawDrag ensured that this is a popup frame. nsCOMPtr widget = frame->GetNearestWidget(); if (widget) { gtkWidget = (GtkWidget*)widget->GetNativeData(NS_NATIVE_SHELLWIDGET); if (gtkWidget) { // When mDragPopup has a parent it's already attached to D&D context. // That may happens when D&D operation is aborted but not finished // on Gtk side yet so let's remove it now. g_object_ref(gtkWidget); if (GtkWidget* parent = gtk_widget_get_parent(gtkWidget)) { gtk_container_remove(GTK_CONTAINER(parent), gtkWidget); } LOGDRAGSERVICE(" set drag popup [%p]", widget.get()); OpenDragPopup(); gtk_drag_set_icon_widget(aContext, gtkWidget, offsetX, offsetY); g_object_unref(gtkWidget); return; } else { LOGDRAGSERVICE(" NS_NATIVE_SHELLWIDGET is missing!"); } } else { LOGDRAGSERVICE(" NearestWidget is missing!"); } } else { LOGDRAGSERVICE(" PrimaryFrame is missing!"); } } if (surface) { LOGDRAGSERVICE(" We have a surface"); if (!SetAlphaPixmap(surface, aContext, offsetX, offsetY, dragRect)) { RefPtr dragPixbuf = nsImageToPixbuf::SourceSurfaceToPixbuf( surface, dragRect.width, dragRect.height); if (dragPixbuf) { LOGDRAGSERVICE(" set drag pixbuf"); gtk_drag_set_icon_pixbuf(aContext, dragPixbuf, offsetX, offsetY); } else { LOGDRAGSERVICE(" SourceSurfaceToPixbuf failed!"); } } } else { LOGDRAGSERVICE(" Surface is missing!"); } } static void invisibleSourceDragBegin(GtkWidget* aWidget, GdkDragContext* aContext, gpointer aData) { LOGDRAGSERVICESTATIC("invisibleSourceDragBegin (%p)", aContext); nsDragSession* dragSession = (nsDragSession*)aData; dragSession->SourceBeginDrag(aContext); dragSession->SetDragIcon(aContext); } static void invisibleSourceDragDataGet(GtkWidget* aWidget, GdkDragContext* aContext, GtkSelectionData* aSelectionData, guint aInfo, guint32 aTime, gpointer aData) { LOGDRAGSERVICESTATIC("invisibleSourceDragDataGet (%p)", aContext); nsDragSession* dragSession = (nsDragSession*)aData; dragSession->SourceDataGet(aWidget, aContext, aSelectionData, aTime); } static gboolean invisibleSourceDragFailed(GtkWidget* aWidget, GdkDragContext* aContext, gint aResult, gpointer aData) { // Wayland and X11 uses different drag results here. When drag target is // missing X11 passes GDK_DRAG_CANCEL_NO_TARGET // (from gdk_dnd_handle_button_event()/gdkdnd-x11.c) // as backend X11 has info about other application windows. // Wayland does not have such info so it always passes // GDK_DRAG_CANCEL_ERROR error code // (see data_source_cancelled/gdkselection-wayland.c). // Bug 1527976 // Emulate what X11 does here as Gecko expect it and handles NO_TARGET // result correctly according to drop destination. if (widget::GdkIsWaylandDisplay() && aResult == GTK_DRAG_RESULT_ERROR) { aResult = GTK_DRAG_RESULT_NO_TARGET; } LOGDRAGSERVICESTATIC("invisibleSourceDragFailed(%p) %s", aContext, kGtkDragResults[aResult]); nsDragSession* dragSession = (nsDragSession*)aData; // End the drag session now (rather than waiting for the drag-end signal) // so that operations performed on dropEffect == none can start immediately // rather than waiting for the drag-failed animation to finish. dragSession->SourceEndDragSession(aContext, aResult); // We should return TRUE to disable the drag-failed animation iff the // source performed an operation when dropEffect was none, but the handler // of the dragend DOM event doesn't provide this information. return FALSE; } static void invisibleSourceDragEnd(GtkWidget* aWidget, GdkDragContext* aContext, gpointer aData) { LOGDRAGSERVICESTATIC("invisibleSourceDragEnd(%p)", aContext); nsDragSession* dragSession = (nsDragSession*)aData; // The drag has ended. Release the hostages! dragSession->SourceEndDragSession(aContext, GTK_DRAG_RESULT_SUCCESS); } // The following methods handle responding to GTK drag signals and // tracking state between these signals. // // In general, GTK does not expect us to run the event loop while handling its // drag signals, however our drag event handlers may run the // event loop, most often to fetch information about the drag data. // // GTK, for example, uses the return value from drag-motion signals to // determine whether drag-leave signals should be sent. If an event loop is // run during drag-motion the XdndLeave message can get processed but when GTK // receives the message it does not yet know that it needs to send the // drag-leave signal to our widget. // // After a drag-drop signal, we need to reply with gtk_drag_finish(). // However, gtk_drag_finish should happen after the drag-drop signal handler // returns so that when the Motif drag protocol is used, the // XmTRANSFER_SUCCESS during gtk_drag_finish is sent after the XmDROP_START // reply sent on return from the drag-drop signal handler. // // Similarly drag-end for a successful drag and drag-failed are not good // times to run a nested event loop as gtk_drag_drop_finished() and // gtk_drag_source_info_destroy() don't gtk_drag_clear_source_info() or remove // drop_timeout until after at least the first of these signals is sent. // Processing other events (e.g. a slow GDK_DROP_FINISHED reply, or the drop // timeout) could cause gtk_drag_drop_finished to be called again with the // same GtkDragSourceInfo, which won't like being destroyed twice. // // Therefore we reply to the signals immediately and schedule a task to // dispatch the Gecko events, which may run the event loop. // // Action in response to drag-leave signals is also delayed until the event // loop runs again so that we find out whether a drag-drop signal follows. // // A single task is scheduled to manage responses to all three GTK signals. // If further signals are received while the task is scheduled, the scheduled // response is updated, sometimes effectively compressing successive signals. // // No Gecko drag events are dispatched (during nested event loops) while other // Gecko drag events are in flight. This helps event handlers that may not // expect nested events, while accessing an event's dataTransfer for example. gboolean nsDragSession::ScheduleMotionEvent(nsWindow* aWindow, GdkDragContext* aDragContext, LayoutDeviceIntPoint aWindowPoint, guint aTime) { if (aDragContext && mScheduledTask == eDragTaskMotion) { // The drag source has sent another motion message before we've // replied to the previous. That shouldn't happen with Xdnd. The // spec for Motif drags is less clear, but we'll just update the // scheduled task with the new position reply only to the most // recent message. NS_WARNING("Drag Motion message received before previous reply was sent"); } // Returning TRUE means we'll reply with a status message, unless we first // get a leave. return Schedule(eDragTaskMotion, aWindow, aDragContext, aWindowPoint, aTime); } void nsDragSession::ScheduleLeaveEvent() { // We don't know at this stage whether a drop signal will immediately // follow. If the drop signal gets sent it will happen before we return // to the main loop and the scheduled leave task will be replaced. if (!Schedule(eDragTaskLeave, nullptr, nullptr, LayoutDeviceIntPoint(), 0)) { NS_WARNING("Drag leave after drop"); } } gboolean nsDragSession::ScheduleDropEvent(nsWindow* aWindow, GdkDragContext* aDragContext, LayoutDeviceIntPoint aWindowPoint, guint aTime) { if (!Schedule(eDragTaskDrop, aWindow, aDragContext, aWindowPoint, aTime)) { NS_WARNING("Additional drag drop ignored"); return FALSE; } SetDragEndPoint(aWindowPoint.x, aWindowPoint.y); // We'll reply with gtk_drag_finish(). return TRUE; } #ifdef MOZ_LOGGING const char* nsDragSession::GetDragServiceTaskName(DragTask aTask) { static const char* taskNames[] = {"eDragTaskNone", "eDragTaskMotion", "eDragTaskLeave", "eDragTaskDrop", "eDragTaskSourceEnd"}; MOZ_ASSERT(size_t(aTask) < std::size(taskNames)); return taskNames[aTask]; } #endif gboolean nsDragSession::Schedule(DragTask aTask, nsWindow* aWindow, GdkDragContext* aDragContext, LayoutDeviceIntPoint aWindowPoint, guint aTime) { // If there is an existing leave or motion task scheduled, then that // will be replaced. When the new task is run, it will dispatch // any necessary leave or motion events. // If aTask is eDragTaskSourceEnd, then it will replace even a scheduled // drop event (which could happen if the drop event has not been processed // within the allowed time). Otherwise, if we haven't yet run a scheduled // drop or end task, just say that we are not ready to receive another // drop. LOGDRAGSERVICE("nsDragSession::Schedule(%p) task %s window %p\n", aDragContext, GetDragServiceTaskName(aTask), aWindow); if (mScheduledTask == eDragTaskSourceEnd || (mScheduledTask == eDragTaskDrop && aTask != eDragTaskSourceEnd)) { LOGDRAGSERVICE(" task does not fit recent task %s, quit!\n", GetDragServiceTaskName(mScheduledTask)); return FALSE; } mScheduledTask = aTask; mPendingWindow = aWindow; mPendingDragContext = aDragContext; mPendingWindowPoint = aWindowPoint; mPendingTime = aTime; if (!mTaskSource) { // High priority is used here because we want to process motion events // right after drag_motion event handler which is called by Gtk. // An ideal scenario is to call TaskDispatchCallback() directly here // but we can't do that. TaskDispatchCallback() spins gtk event loop // while nsDragSession::Schedule() is already called from event loop // (by drag_motion* gtk_widget events) so that direct call will cause // nested recursion. mTaskSource = g_timeout_add_full(G_PRIORITY_HIGH, 0, TaskDispatchCallback, this, nullptr); } // We need to reply to drag_motion event on Wayland immediately, // see Bug 1730203. if (widget::GdkIsWaylandDisplay() && mScheduledTask == eDragTaskMotion) { UpdateDragAction(aDragContext); ReplyToDragMotion(aDragContext, aTime); } return TRUE; } gboolean nsDragSession::TaskDispatchCallback(gpointer data) { // Make sure we hold a strong reference to the session while we process // the task. RefPtr dragSession = static_cast(data); AutoEventLoop loop(dragSession); return dragSession->RunScheduledTask(); } gboolean nsDragSession::RunScheduledTask() { LOGDRAGSERVICE( "nsDragSession::RunScheduledTask() task %s mTargetWindow %p " "mPendingWindow %p\n", GetDragServiceTaskName(mScheduledTask), mTargetWindow.get(), mPendingWindow.get()); // Don't run RunScheduledTask() twice. As we use it in main thread only // we don't need to be thread safe here. if (mScheduledTaskIsRunning) { LOGDRAGSERVICE(" sheduled task is already running, quit."); return FALSE; } AutoRestore guard(mScheduledTaskIsRunning); mScheduledTaskIsRunning = true; if (mTargetWindow && mTargetWindow != mPendingWindow) { LOGDRAGSERVICE(" dispatch eDragExit (%p)\n", mTargetWindow.get()); mTargetWindow->DispatchDragEvent(eDragExit, mTargetWindowPoint, 0); if (!mSourceNode) { // The drag that was initiated in a different app. End the drag // session, since we're done with it for now (until the user drags // back into this app). EndDragSession(false, GetCurrentModifiers()); } } // It is possible that the pending state has been updated during dispatch // of the leave event. That's fine. // Now we collect the pending state because, from this point on, we want // to use the same state for all events dispatched. All state is updated // so that when other tasks are scheduled during dispatch here, this // task is considered to have already been run. bool positionHasChanged = mPendingWindow != mTargetWindow || mPendingWindowPoint != mTargetWindowPoint; DragTask task = mScheduledTask; mScheduledTask = eDragTaskNone; mTargetWindow = std::move(mPendingWindow); mTargetWindowPoint = mPendingWindowPoint; if (task == eDragTaskLeave || task == eDragTaskSourceEnd) { LOGDRAGSERVICE(" quit, selected task %s\n", GetDragServiceTaskName(task)); if (task == eDragTaskSourceEnd) { // Dispatch drag end events. EndDragSession(true, GetCurrentModifiers()); } // Nothing more to do // Returning false removes the task source from the event loop. mTaskSource = 0; return FALSE; } // mTargetWidget may be nullptr if the window has been destroyed. // (The leave event is not scheduled if a drop task is still scheduled.) // We still reply appropriately to indicate that the drop will or didn't // succeeed. mTargetWidget = mTargetWindow ? mTargetWindow->GetGtkWidget() : nullptr; LOGDRAGSERVICE(" start drag session mTargetWindow %p mTargetWidget %p\n", mTargetWindow.get(), mTargetWidget.get()); LOGDRAGSERVICE(" mPendingDragContext %p => mTargetDragContext %p\n", mPendingDragContext.get(), mTargetDragContext.get()); mTargetDragContext = std::move(mPendingDragContext); mTargetTime = mPendingTime; SetCachedDragContext(mTargetDragContext); // http://www.whatwg.org/specs/web-apps/current-work/multipage/dnd.html#drag-and-drop-processing-model // (as at 27 December 2010) indicates that a "drop" event should only be // fired (at the current target element) if the current drag operation is // not none. The current drag operation will only be set to a non-none // value during a "dragover" event. // // If the user has ended the drag before any dragover events have been // sent, then the spec recommends skipping the drop (because the current // drag operation is none). However, here we assume that, by releasing // the mouse button, the user has indicated that they want to drop, so we // proceed with the drop where possible. // // In order to make the events appear to content in the same way as if the // spec is being followed we make sure to dispatch a "dragover" event with // appropriate coordinates and check canDrop before the "drop" event. // // When the Xdnd protocol is used for source/destination communication (as // should be the case with GTK source applications) a dragover event // should have already been sent during the drag-motion signal, which // would have already been received because XdndDrop messages do not // contain a position. However, we can't assume the same when the Motif // protocol is used. if (task == eDragTaskMotion || positionHasChanged) { LOGDRAGSERVICE(" process motion event\n"); UpdateDragAction(); TakeDragEventDispatchedToChildProcess(); // Clear the old value. DispatchMotionEvents(); if (task == eDragTaskMotion) { if (TakeDragEventDispatchedToChildProcess()) { mTargetDragContextForRemote = mTargetDragContext; } else { // Reply to tell the source whether we can drop and what // action would be taken. ReplyToDragMotion(); } } } if (task == eDragTaskDrop) { LOGDRAGSERVICE(" process drop task\n"); gboolean success = DispatchDropEvent(); // Perhaps we should set the del parameter to TRUE when the drag // action is move, but we don't know whether the data was successfully // transferred. if (mTargetDragContext) { LOGDRAGSERVICE(" drag finished (gtk_drag_finish)"); gtk_drag_finish(mTargetDragContext, success, /* del = */ FALSE, mTargetTime); } // Make sure to end the drag session. If this drag started in a // different app, we won't get a drag_end signal to end it from. EndDragSession(true, GetCurrentModifiers()); } // We're done with the drag context. LOGDRAGSERVICE(" clear mTargetWindow mTargetWidget and other data\n"); mTargetWidget = nullptr; mTargetDragContext = nullptr; // If we got another drag signal while running the sheduled task, that // must have happened while running a nested event loop. Leave the task // source on the event loop. if (mScheduledTask != eDragTaskNone) return TRUE; // We have no task scheduled. // Returning false removes the task source from the event loop. LOGDRAGSERVICE(" remove task source\n"); mTaskSource = 0; return FALSE; } // This will update the drag action based on the information in the // drag context. Gtk gets this from a combination of the key settings // and what the source is offering. void nsDragSession::UpdateDragAction(GdkDragContext* aDragContext) { // This doesn't look right. dragSession.dragAction is used by // nsContentUtils::SetDataTransferInEvent() to set the initial // dataTransfer.dropEffect, so GdkDragContext::suggested_action would be // more appropriate. GdkDragContext::actions should be used to set // dataTransfer.effectAllowed, which doesn't currently happen with // external sources. LOGDRAGSERVICE("nsDragSession::UpdateDragAction(%p)", aDragContext); // default is to do nothing int action = nsIDragService::DRAGDROP_ACTION_NONE; GdkDragAction gdkAction = GDK_ACTION_DEFAULT; if (aDragContext) { gdkAction = gdk_drag_context_get_actions(aDragContext); LOGDRAGSERVICE(" gdk_drag_context_get_actions() returns 0x%X", gdkAction); // When D&D modifiers (CTRL/SHIFT) are involved, // gdk_drag_context_get_actions() on X11 returns selected action but // Wayland returns all allowed actions. // So we need to call gdk_drag_context_get_selected_action() on Wayland // to get potential D&D modifier. // gdk_drag_context_get_selected_action() is also affected by // gdk_drag_status(), see nsDragSession::ReplyToDragMotion(). if (widget::GdkIsWaylandDisplay()) { GdkDragAction gdkActionSelected = gdk_drag_context_get_selected_action(aDragContext); LOGDRAGSERVICE(" gdk_drag_context_get_selected_action() returns 0x%X", gdkActionSelected); if (gdkActionSelected) { gdkAction = gdkActionSelected; } } } // set the default just in case nothing matches below if (gdkAction & GDK_ACTION_DEFAULT) { LOGDRAGSERVICE(" set default move"); action = nsIDragService::DRAGDROP_ACTION_MOVE; } // first check to see if move is set if (gdkAction & GDK_ACTION_MOVE) { LOGDRAGSERVICE(" set explicit move"); action = nsIDragService::DRAGDROP_ACTION_MOVE; } else if (gdkAction & GDK_ACTION_LINK) { // then fall to the others LOGDRAGSERVICE(" set explicit link"); action = nsIDragService::DRAGDROP_ACTION_LINK; } else if (gdkAction & GDK_ACTION_COPY) { // copy is ctrl LOGDRAGSERVICE(" set explicit copy"); action = nsIDragService::DRAGDROP_ACTION_COPY; } // update the drag information SetDragAction(action); } void nsDragSession::UpdateDragAction() { UpdateDragAction(mTargetDragContext); } NS_IMETHODIMP nsDragSession::UpdateDragEffect() { LOGDRAGSERVICE("nsDragSession::UpdateDragEffect() from e10s child process"); if (mTargetDragContextForRemote) { ReplyToDragMotion(mTargetDragContextForRemote, mTargetTime); mTargetDragContextForRemote = nullptr; } return NS_OK; } void nsDragSession::ReplyToDragMotion() { if (mTargetDragContext) { ReplyToDragMotion(mTargetDragContext, mTargetTime); } } void nsDragSession::DispatchMotionEvents() { if (mSourceWindow) { FireDragEventAtSource(eDrag, GetCurrentModifiers()); } if (mTargetWindow) { mTargetWindow->DispatchDragEvent(eDragOver, mTargetWindowPoint, mTargetTime); } } // Returns true if the drop was successful gboolean nsDragSession::DispatchDropEvent() { // We need to check IsDestroyed here because the nsRefPtr // only protects this from being deleted, it does NOT protect // against nsView::~nsView() calling Destroy() on it, bug 378273. if (!mTargetWindow || mTargetWindow->IsDestroyed()) { return FALSE; } EventMessage msg = mCanDrop ? eDrop : eDragExit; mTargetWindow->DispatchDragEvent(msg, mTargetWindowPoint, mTargetTime); return mCanDrop; } /* static */ uint32_t nsDragSession::GetCurrentModifiers() { return mozilla::widget::KeymapWrapper::ComputeCurrentKeyModifiers(); } nsAutoCString nsDragSession::GetDebugTag() const { nsAutoCString tag; tag.AppendPrintf("[%p]", this); return tag; } #undef LOGDRAGSERVICE