/* 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 "nsDragSessionSource.h" #include #include #include "GRefPtr.h" #include "mozilla/Logging.h" #include "mozilla/PresShell.h" #include "mozilla/Services.h" #include "mozilla/StaticPrefs_widget.h" #include "mozilla/WidgetUtilsGtk.h" #include "nsAppShell.h" #include "nsArrayUtils.h" #include "nsCRT.h" #include "nsComponentManagerUtils.h" #include "nsICookieJarSettings.h" #include "nsIFileURL.h" #include "nsIIOService.h" #include "nsISupportsPrimitives.h" #include "nsNetUtil.h" #include "nsPrimitiveHelpers.h" #include "nsSystemInfo.h" #include "nsWidgetsCID.h" #include "nsWindow.h" #ifdef MOZ_X11 # include "gfxXlibSurface.h" #endif #include "ScreenHelperGTK.h" #include "gfxPlatform.h" #include "mozilla/dom/Document.h" #include "mozilla/gfx/2D.h" #include "mozilla/widget/nsGtkHtmlUtils.h" #include "nsDirectoryServiceDefs.h" #include "nsEscape.h" #include "nsGtkUtils.h" #include "nsIContent.h" #include "nsImageToPixbuf.h" #include "nsPresContext.h" #include "nsStringStream.h" using namespace mozilla; using namespace mozilla::widget; using namespace mozilla::gfx; #define NS_DND_TMP_CLEANUP_TIMEOUT (1000 * 60 * 5) #define NS_SYSTEMINFO_CONTRACTID "@mozilla.org/system-info;1" #define DRAG_IMAGE_ALPHA_LEVEL 0.5 #undef LOGDRAGSERVICE #undef LOGDRAGSERVICESTATIC #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(...) # define LOGDRAGSERVICESTATIC(...) #endif class MOZ_STACK_CLASS AutoSuspendNativeEvents { public: AutoSuspendNativeEvents() { mAppShell = do_GetService(NS_APPSHELL_CID); mAppShell->SuspendNative(); } ~AutoSuspendNativeEvents() { mAppShell->ResumeNative(); } private: nsCOMPtr mAppShell; }; 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, }; // 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 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; } 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 GtkWindow* GetGtkWindow(dom::Document* aDocument) { if (!aDocument) return nullptr; PresShell* presShell = aDocument->GetPresShell(); if (!presShell) { return nullptr; } nsCOMPtr widget = presShell->GetRootWidget(); if (!widget) { return nullptr; } GtkWidget* gtkWidget = GTK_WIDGET(widget->GetNativeData(NS_NATIVE_SHELLWIDGET)); if (!gtkWidget) return nullptr; GtkWidget* toplevel = nullptr; toplevel = gtk_widget_get_toplevel(gtkWidget); if (!GTK_IS_WINDOW(toplevel)) return nullptr; return GTK_WINDOW(toplevel); } static nsresult GetDownloadDetails(nsITransferable* aTransferable, nsIURI** aSourceURI, nsAString& aFilename) { *aSourceURI = nullptr; MOZ_ASSERT(aTransferable != nullptr, "aTransferable must not be null"); 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 = std::move(srcFileName); return NS_OK; } // Support for periodic drag events static gboolean DispatchMotionEventCopy(gpointer aData) { sMotionEventTimerID = 0; GUniquePtr event = TakeMotionEvent(); if (gtk_widget_has_grab(sGrabWidget)) { gtk_propagate_event(sGrabWidget, event.get()); } return FALSE; } static void OnSourceGrabEventAfter(GtkWidget* widget, GdkEvent* event, gpointer user_data) { if (!gtk_widget_has_grab(sGrabWidget)) return; if (event->type == GDK_MOTION_NOTIFY) { SetMotionEvent(GUniquePtr(gdk_event_copy(event))); nsDragSessionSource* session = static_cast(user_data); 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)) { sMotionEvent->motion.state = event->key.state; } else { return; } if (sMotionEventTimerID) { g_source_remove(sMotionEventTimerID); } sMotionEventTimerID = g_timeout_add_full( G_PRIORITY_DEFAULT_IDLE, 350, DispatchMotionEventCopy, nullptr, nullptr); } nsDragSessionSource::nsDragSessionSource() { LOGDRAGSERVICE("nsDragSessionSource::nsDragSessionSource()"); // Using an offscreen window works around bug 983843. mHiddenWidget = gtk_offscreen_window_new(); gtk_widget_realize(mHiddenWidget); 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); mTempFileTimerID = 0; #ifdef MOZ_X11 mActive = widget::GdkIsX11Display(); #endif } nsDragSessionSource::~nsDragSessionSource() { LOGDRAGSERVICE("nsDragSessionSource::~nsDragSessionSource"); MozClearHandleID(mTempFileTimerID, g_source_remove); RemoveTempFiles(); MozClearPointer(mHiddenWidget, gtk_widget_destroy); } NS_IMETHODIMP nsDragSessionSource::InvokeDragSession( nsIWidget* aWidget, nsINode* aDOMNode, nsIPrincipal* aPrincipal, nsIPolicyContainer* aPolicyContainer, nsICookieJarSettings* aCookieJarSettings, nsIArray* aArrayTransferables, uint32_t aActionType, nsContentPolicyType aContentPolicyType = nsIContentPolicy::TYPE_OTHER) { LOGDRAGSERVICE("nsDragSessionSource::InvokeDragSession"); if (mSourceNode) return NS_ERROR_NOT_AVAILABLE; return nsBaseDragSession::InvokeDragSession( aWidget, aDOMNode, aPrincipal, aPolicyContainer, aCookieJarSettings, aArrayTransferables, aActionType, aContentPolicyType); } nsresult nsDragSessionSource::InvokeDragSessionImpl( nsIWidget* aWidget, nsIArray* aArrayTransferables, const Maybe& aRegion, uint32_t aActionType) { if (!aArrayTransferables) return NS_ERROR_INVALID_ARG; mSourceDataItems = aArrayTransferables; GdkDevice* device = widget::GdkGetPointer(); GdkWindow* originGdkWindow = nullptr; if (widget::GdkIsWaylandDisplay() || widget::IsXWaylandProtocol()) { originGdkWindow = gdk_device_get_window_at_position(device, nullptr, nullptr); if (!originGdkWindow) { NS_WARNING( "nsDragSessionSource::InvokeDragSessionImpl(): Missing origin " "GdkWindow!"); return NS_ERROR_FAILURE; } } #ifdef MOZ_WAYLAND if (widget::GdkIsWaylandDisplay() && !gdk_wayland_window_get_wl_surface(originGdkWindow)) { NS_WARNING( "nsDragSessionSource::InvokeDragSessionImpl(): Missing origin " "wl_surface!"); return NS_ERROR_FAILURE; } #endif GtkTargetList* sourceList = GetSourceList(); if (!sourceList) { return NS_OK; } 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::GetLastPointerDownEvent(); GdkEvent fakeEvent; if (!existingEvent) { 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; } GtkWindowGroup* window_group = gtk_window_get_group(GetGtkWindow(mSourceDocument)); gtk_window_group_add_window(window_group, GTK_WINDOW(mHiddenWidget)); LOGDRAGSERVICE( "nsDragSessionSource::InvokeDragSessionImpl() originGdkWindow [%p]", originGdkWindow); 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) { sGrabWidget = gtk_window_group_get_current_grab(window_group); if (sGrabWidget) { g_object_ref(sGrabWidget); g_signal_connect(sGrabWidget, "event-after", G_CALLBACK(OnSourceGrabEventAfter), this); } mEndDragPoint = LayoutDeviceIntPoint(-1, -1); rv = NS_OK; } else { rv = NS_ERROR_FAILURE; } gtk_target_list_unref(sourceList); return rv; } nsresult nsDragSessionSource::EndDragSessionImpl(bool aDoneDrag, uint32_t aKeyModifiers) { LOGDRAGSERVICE("nsDragSessionSource::EndDragSessionImpl() %d", 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(); } } // start timer to remove temporary files if (mTemporaryFiles.Count() > 0 && !mTempFileTimerID) { LOGDRAGSERVICE(" queue removing of temporary files"); 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; } return nsDragSession::EndDragSessionImpl(aDoneDrag, aKeyModifiers); } void nsDragSessionSource::EndDragSessionMainThread() { EndDragSession(true, nsDragSession::GetCurrentModifiers()); } bool nsDragSessionSource::RemoveTempFiles() { LOGDRAGSERVICE("nsDragSessionSource::RemoveTempFiles"); 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; } /* static */ gboolean nsDragSessionSource::TaskRemoveTempFiles(gpointer data) { RefPtr session = static_cast(data); session.get()->Release(); return session->RemoveTempFiles(); } void nsDragSessionSource::SourceEndDragSession(GdkDragContext* aContext, gint aResult) { LOGDRAGSERVICE("SourceEndDragSession(%p) result %s\n", aContext, kGtkDragResults[aResult]); mSourceDataItems = nullptr; GdkAtom property = sXdndDirectSaveTypeAtom; gdk_property_delete(gdk_drag_context_get_source_window(aContext), property); if (!mDoingDrag || mDragTaskSourceFinished) return; if (mEndDragPoint.x < 0) { 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); } uint32_t dropEffect; if (aResult == GTK_DRAG_RESULT_SUCCESS) { LOGDRAGSERVICE(" drop is accepted"); GdkDragAction action = gdk_drag_context_get_dest_window(aContext) ? gdk_drag_context_get_actions(aContext) : (GdkDragAction)0; 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); } mDragTaskSourceFinished = true; NS_DispatchToMainThread( NewRunnableMethod("nsDragSessionSource::EndDragSession", this, &nsDragSessionSource::EndDragSessionMainThread)); } GtkTargetList* nsDragSessionSource::GetSourceList(void) { if (!mSourceDataItems) { return nullptr; } nsTArray targetArray; GtkTargetEntry* targets; GtkTargetList* targetList = nullptr; uint32_t targetCount = 0; unsigned int numDragItems = 0; mSourceDataItems->GetLength(&numDragItems); LOGDRAGSERVICE(" numDragItems = %d", numDragItems); if (numDragItems > 1) { TargetArrayAddTarget(targetArray, gMimeListType); nsCOMPtr currItem = do_QueryElementAt(mSourceDataItems, 0); if (currItem) { nsTArray flavors; currItem->FlavorsTransferableCanExport(flavors); for (uint32_t i = 0; i < flavors.Length(); ++i) { if (flavors[i].EqualsLiteral(kURLMime)) { TargetArrayAddTarget(targetArray, gTextUriListType); break; } } } } 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 (requestedFlavor == sFileMimeAtom) { TargetArrayAddTarget(targetArray, gTextUriListType); } else if (requestedFlavor == sTextMimeAtom) { TargetArrayAddTarget(targetArray, gTextPlainUTF8Type); TargetArrayAddTarget(targetArray, gUTF8STRINGType); TargetArrayAddTarget(targetArray, gSTRINGType); } 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); } } } else if (requestedFlavor == sFilePromiseURLMimeAtom) { TargetArrayAddTarget(targetArray, gTextUriListType); } else if (widget::GdkIsX11Display() && !widget::IsXWaylandProtocol() && requestedFlavor == sFilePromiseMimeAtom) { TargetArrayAddTarget(targetArray, gXdndDirectSaveType); } else if (requestedFlavor == sNativeImageMimeAtom) { TargetArrayAddTarget(targetArray, kPNGImageMime); TargetArrayAddTarget(targetArray, kJPEGImageMime); TargetArrayAddTarget(targetArray, kJPGImageMime); TargetArrayAddTarget(targetArray, kGIFImageMime); } } } } targetCount = targetArray.Length(); if (targetCount) { targets = (GtkTargetEntry*)g_malloc(sizeof(GtkTargetEntry) * targetCount); uint32_t targetIndex; for (targetIndex = 0; targetIndex < targetCount; ++targetIndex) { GtkTargetEntry* disEntry = targetArray.ElementAt(targetIndex); targets[targetIndex].target = disEntry->target; targets[targetIndex].flags = disEntry->flags; targets[targetIndex].info = 0; } targetList = gtk_target_list_new(targets, targetCount); for (uint32_t cleanIndex = 0; cleanIndex < targetCount; ++cleanIndex) { GtkTargetEntry* thisTarget = targetArray.ElementAt(cleanIndex); g_free(thisTarget->target); g_free(thisTarget); } g_free(targets); } else { targetList = gtk_target_list_new(nullptr, 0); } return targetList; } nsresult nsDragSessionSource::CreateTempFile(nsITransferable* aItem, nsACString& aURI) { LOGDRAGSERVICE("nsDragSessionSource::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; 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; } 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; } } 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; } 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; } nsCOMPtr tempFile; tmpDir->Clone(getter_AddRefs(tempFile)); mTemporaryFiles.AppendObject(tempFile); MozClearHandleID(mTempFileTimerID, g_source_remove); 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 (true) { 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; } mTempFileUrls.AppendElement()->Assign(aURI); LOGDRAGSERVICE(" storing tmp file as %s", PromiseFlatCString(aURI).get()); return NS_OK; } bool nsDragSessionSource::SourceDataAppendURLFileItem(nsACString& aURI, nsITransferable* aItem) { 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 nsDragSessionSource::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; } if (SourceDataAppendURLFileItem(aURI, aItem)) { return true; } nsCOMPtr promiseData; rv = aItem->GetTransferData(kFilePromiseURLMime, getter_AddRefs(promiseData)); NS_ENSURE_SUCCESS(rv, false); return NS_SUCCEEDED(CreateTempFile(aItem, aURI)); } void nsDragSessionSource::SourceDataGetUriList(GdkDragContext* aContext, GtkSelectionData* aSelectionData, uint32_t aDragItems) { const bool isExternalDrop = widget::GdkIsX11Display() ? !nsWindow::GetWindow(gdk_drag_context_get_dest_window(aContext)) : !gdk_drag_context_get_dest_window(aContext); LOGDRAGSERVICE( "nsDragSessionSource::SourceDataGetUriLists() len %d external %d", aDragItems, isExternalDrop); 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; } 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 nsDragSessionSource::SourceDataGetImage(nsITransferable* aItem, GtkSelectionData* aSelectionData) { LOGDRAGSERVICE("nsDragSessionSource::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 nsDragSessionSource::SourceDataGetXDND(nsITransferable* aItem, GdkDragContext* aContext, GtkSelectionData* aSelectionData) { LOGDRAGSERVICE("nsDragSessionSource::SourceDataGetXDND"); 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; } 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 (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."); 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; } 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); gtk_selection_data_set(aSelectionData, target, 8, (guchar*)"S", 1); return true; } bool nsDragSessionSource::SourceDataGetText(nsITransferable* aItem, const nsACString& aMIMEType, bool aNeedToDoConversionToPlainText, GtkSelectionData* aSelectionData) { LOGDRAGSERVICE("nsDragSessionSource::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 (aNeedToDoConversionToPlainText) { char* plainTextData = nullptr; char16_t* castedUnicode = reinterpret_cast(tmpData); uint32_t plainTextLen = 0; UTF16ToNewUTF8(castedUnicode, tmpDataLen / 2, &plainTextData, &plainTextLen); if (tmpData) { free(tmpData); tmpData = plainTextData; tmpDataLen = plainTextLen; } } if (tmpData) { GdkAtom target = gtk_selection_data_get_target(aSelectionData); gtk_selection_data_set(aSelectionData, target, 8, (guchar*)tmpData, tmpDataLen); free(tmpData); } return true; } void nsDragSessionSource::SourceDataGet(GtkWidget* aWidget, GdkDragContext* aContext, GtkSelectionData* aSelectionData, guint32 aTime) { GdkAtom requestedFlavor = gtk_selection_data_get_target(aSelectionData); LOGDRAGSERVICE("nsDragSessionSource::SourceDataGet(%p) MIME %s", aContext, GUniquePtr(gdk_atom_name(requestedFlavor)).get()); 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 (IsTextFlavor(requestedFlavor)) { if (!SourceDataGetText(item, nsDependentCString(kTextMime), /* aNeedToDoConversionToPlainText */ true, aSelectionData)) { LOGDRAGSERVICE( " Failed to send sTextMimeAtom/sTextPlainUTF8TypeAtom data!"); } return; } else if (requestedFlavor == sXdndDirectSaveTypeAtom) { if (!SourceDataGetXDND(item, aContext, aSelectionData)) { LOGDRAGSERVICE(" Failed to send sXdndDirectSaveTypeAtom data!"); } return; } else if (requestedFlavor == sPNGImageMimeAtom || requestedFlavor == sJPEGImageMimeAtom || requestedFlavor == sJPGImageMimeAtom || requestedFlavor == sGIFImageMimeAtom) { if (!SourceDataGetImage(item, aSelectionData)) { LOGDRAGSERVICE(" Failed to send image data!"); } return; } else if (requestedFlavor == sMozUrlTypeAtom) { if (SourceDataGetText(item, nsDependentCString(kURLMime), /* aNeedToDoConversionToPlainText */ true, aSelectionData)) { LOGDRAGSERVICE(" Failed to send kURLMime data!"); return; } } GUniquePtr flavorName(gdk_atom_name(requestedFlavor)); if (nsDependentCString(flavorName.get()).EqualsLiteral(kHTMLMime)) { nsCOMPtr data; if (NS_FAILED(item->GetTransferData(kHTMLMime, getter_AddRefs(data))) || !data) { LOGDRAGSERVICE(" Failed to get kHTMLMime data!"); return; } nsCOMPtr wideString = do_QueryInterface(data); if (!wideString) { LOGDRAGSERVICE(" kHTMLMime data is not nsISupportsString"); return; } nsAutoString ucs2string; wideString->GetData(ucs2string); nsAutoCString html; html.AppendLiteral(mozilla::widget::kHTMLMarkupPrefix); AppendUTF16toUTF8(ucs2string, html); GdkAtom target = gtk_selection_data_get_target(aSelectionData); gtk_selection_data_set(aSelectionData, target, 8, (const guchar*)html.get(), html.Length()); return; } if (!SourceDataGetText(item, nsDependentCString(flavorName.get()), /* aNeedToDoConversionToPlainText */ false, aSelectionData)) { LOGDRAGSERVICE(" Failed to send %s data!", nsDependentCString(flavorName.get()).get()); } } void nsDragSessionSource::SourceBeginDrag(GdkDragContext* aContext) { LOGDRAGSERVICE("nsDragSessionSource::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; } } } bool nsDragSessionSource::SetAlphaPixmap(SourceSurface* aSurface, GdkDragContext* aContext, int32_t aXOffset, int32_t aYOffset, const LayoutDeviceIntRect& dragRect) { GdkScreen* screen = gtk_widget_get_screen(mHiddenWidget); 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 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); 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; } void nsDragSessionSource::SetDragIcon(GdkDragContext* aContext) { if (!mHasImage && !mSelection) return; LOGDRAGSERVICE("nsDragSessionSource::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; 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) { nsCOMPtr widget = frame->GetNearestWidget(); if (widget) { gtkWidget = (GtkWidget*)widget->GetNativeData(NS_NATIVE_SHELLWIDGET); if (gtkWidget) { 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); nsDragSessionSource* dragSession = (nsDragSessionSource*)aData; // Keep nsDragSessionSource until D&D is finished. dragSession->AddRef(); 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); nsDragSessionSource* dragSession = (nsDragSessionSource*)aData; dragSession->SourceDataGet(aWidget, aContext, aSelectionData, aTime); } static gboolean invisibleSourceDragFailed(GtkWidget* aWidget, GdkDragContext* aContext, gint aResult, gpointer aData) { if (widget::GdkIsWaylandDisplay() && aResult == GTK_DRAG_RESULT_ERROR) { aResult = GTK_DRAG_RESULT_NO_TARGET; } LOGDRAGSERVICESTATIC("invisibleSourceDragFailed(%p) %s", aContext, kGtkDragResults[aResult]); nsDragSessionSource* dragSession = (nsDragSessionSource*)aData; dragSession->SourceEndDragSession(aContext, aResult); return FALSE; } static void invisibleSourceDragEnd(GtkWidget* aWidget, GdkDragContext* aContext, gpointer aData) { LOGDRAGSERVICESTATIC("invisibleSourceDragEnd(%p)", aContext); // Release reference taken at invisibleSourceDragBegin. RefPtr dragSession = dont_AddRef(static_cast(aData)); dragSession->SourceEndDragSession(aContext, GTK_DRAG_RESULT_SUCCESS); } #undef LOGDRAGSERVICE #undef LOGDRAGSERVICESTATIC