/* 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 "nsClipboard.h" #include #include // shellapi.h is needed to build with WIN32_LEAN_AND_MEAN #include #include #ifdef ACCESSIBILITY # include "mozilla/a11y/Compatibility.h" #endif #include "SpecialSystemDirectory.h" #include "WinOLELock.h" #include "WinUtils.h" #include "imgIContainer.h" #include "imgITools.h" #include "mozilla/CheckedInt.h" #include "mozilla/Logging.h" #include "mozilla/ScopeExit.h" #include "mozilla/StaticPrefs_clipboard.h" #include "mozilla/StaticPrefs_widget.h" #include "mozilla/WindowsVersion.h" #include "mozilla/widget/WebCustomFormatUtils.h" #include "nsArrayUtils.h" #include "nsCOMPtr.h" #include "nsCRT.h" #include "nsComponentManagerUtils.h" #include "nsDataObj.h" #include "nsEscape.h" #include "nsIFileProtocolHandler.h" #include "nsIInputStream.h" #include "nsIObserverService.h" #include "nsISupportsPrimitives.h" #include "nsITransferable.h" #include "nsIWidget.h" #include "nsMimeTypes.h" #include "nsNativeCharsetUtils.h" #include "nsNetUtil.h" #include "nsPrimitiveHelpers.h" #include "nsReadableUtils.h" #include "nsString.h" #include "nsUnicharUtils.h" #include "nsWidgetsCID.h" #include "nsXPCOM.h" /* static */ UINT nsClipboard::GetClipboardFileDescriptorFormatA() { static UINT format = ::RegisterClipboardFormatW(CFSTR_FILEDESCRIPTORA); MOZ_ASSERT(format); return format; } /* static */ UINT nsClipboard::GetClipboardFileDescriptorFormatW() { static UINT format = ::RegisterClipboardFormatW(CFSTR_FILEDESCRIPTORW); MOZ_ASSERT(format); return format; } /* static */ UINT nsClipboard::GetHtmlClipboardFormat() { static UINT format = ::RegisterClipboardFormatW(L"HTML Format"); return format; } /* static */ UINT nsClipboard::GetCustomClipboardFormat() { static UINT format = ::RegisterClipboardFormatW(L"application/x-moz-custom-clipdata"); return format; } /* static */ UINT nsClipboard::GetWebCustomFormatMapClipboardFormat() { static UINT format = ::RegisterClipboardFormatW(L"Web Custom Format Map"); MOZ_ASSERT(format); return format; } static inline nsresult CheckClipboardByteSize(HGLOBAL aHGlobal, uint64_t aByteThreshold) { // GlobalSize returns the size of the heap allocation backing aHGlobal, // which may be slightly larger than the actual clipboard data. if (::GlobalSize(aHGlobal) > aByteThreshold) { return NS_ERROR_CLIPBOARD_TOO_BIG; } return NS_OK; } // Reads the "Web Custom Format Map" clipboard format and decodes its JSON // into the supplied map. Fetches via aDataObject (OLE) when provided; falls // back to the legacy Windows clipboard API rooted at aWindow when aDataObject // is null, matching the same primary/fallback path other flavors use in // GetDataFromDataObject(). Returns false if the format is missing or the // payload fails to parse. static bool GetWebCustomFormatMapFromClipboard( IDataObject* aDataObject, nsIWidget* aWindow, mozilla::widget::WebCustomFormatMap& aMap) { const UINT format = nsClipboard::GetWebCustomFormatMapClipboardFormat(); void* data = nullptr; uint32_t dataLen = 0; nsresult rv = NS_ERROR_FAILURE; if (aDataObject) { rv = nsClipboard::GetNativeDataOffClipboard( aDataObject, 0, format, /* aMIMEFlavor */ nullptr, &data, &dataLen); } else if (aWindow) { rv = nsClipboard::GetNativeDataOffClipboard(aWindow, 0, format, &data, &dataLen); } if (NS_FAILED(rv) || !data) { return false; } bool ok = mozilla::widget::JSONToWebCustomFormatMap( nsDependentCSubstring(static_cast(data), dataLen), aMap); free(data); return ok; } //------------------------------------------------------------------------- // // nsClipboard constructor // //------------------------------------------------------------------------- nsClipboard::nsClipboard() : nsBaseClipboard(mozilla::dom::ClipboardCapabilities( false /* supportsSelectionClipboard */, false /* supportsFindClipboard */, false /* supportsSelectionCache */)) { mWindow = nullptr; // Register for a shutdown notification so that we can flush data // to the OS clipboard. nsCOMPtr observerService = do_GetService("@mozilla.org/observer-service;1"); if (observerService) { observerService->AddObserver(this, NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID, false); } } //------------------------------------------------------------------------- // nsClipboard destructor //------------------------------------------------------------------------- nsClipboard::~nsClipboard() {} NS_IMPL_ISUPPORTS_INHERITED(nsClipboard, nsBaseClipboard, nsIObserver) NS_IMETHODIMP nsClipboard::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) { // This will be called on shutdown. ::OleFlushClipboard(); ::CloseClipboard(); return NS_OK; } //------------------------------------------------------------------------- UINT nsClipboard::GetFormat(const char* aMimeStr, bool aMapHTMLMime) { UINT format; if (strcmp(aMimeStr, kTextMime) == 0) { format = CF_UNICODETEXT; } else if (strcmp(aMimeStr, kRTFMime) == 0) { format = ::RegisterClipboardFormat(L"Rich Text Format"); } else if (strcmp(aMimeStr, kJPEGImageMime) == 0 || strcmp(aMimeStr, kJPGImageMime) == 0) { format = CF_DIBV5; } else if (strcmp(aMimeStr, kPNGImageMime) == 0) { format = ::RegisterClipboardFormat(TEXT("PNG")); } else if (strcmp(aMimeStr, kFileMime) == 0 || strcmp(aMimeStr, kFilePromiseMime) == 0) { format = CF_HDROP; } else if ((strcmp(aMimeStr, kNativeHTMLMime) == 0) || (aMapHTMLMime && strcmp(aMimeStr, kHTMLMime) == 0)) { format = GetHtmlClipboardFormat(); } else if (strcmp(aMimeStr, kCustomTypesMime) == 0) { format = GetCustomClipboardFormat(); } else if (strcmp(aMimeStr, kWebCustomFormatMapType) == 0) { format = GetWebCustomFormatMapClipboardFormat(); } else { format = ::RegisterClipboardFormatW(NS_ConvertASCIItoUTF16(aMimeStr).get()); } return format; } mozilla::Maybe nsClipboard::GetSecondaryFormat(const char* aMimeStr) { if (strcmp(aMimeStr, kPNGImageMime) == 0) { // Fall back to DIBV5 format return mozilla::Some(CF_DIBV5); } return mozilla::Nothing(); } template /* static */ bool nsClipboard::FileGroupDescriptorHasItems(HGLOBAL aHGlobal, uint64_t aItemCount) { if (!aHGlobal) { return false; } using FileDesc = std::remove_extent_t; mozilla::CheckedInt requiredSize(offsetof(GroupDesc, fgd)); requiredSize += mozilla::CheckedInt(aItemCount) * sizeof(FileDesc); return requiredSize.isValid() && ::GlobalSize(aHGlobal) >= requiredSize.value(); } template bool nsClipboard::FileGroupDescriptorHasItems( HGLOBAL, uint64_t); template bool nsClipboard::FileGroupDescriptorHasItems( HGLOBAL, uint64_t); //------------------------------------------------------------------------- // static nsresult nsClipboard::CreateNativeDataObject( nsITransferable* aTransferable, IDataObject** aDataObj, nsIURI* aUri, MightNeedToFlush* aMightNeedToFlush) { MOZ_ASSERT(aTransferable); if (!aTransferable) { return NS_ERROR_FAILURE; } // Create our native DataObject that implements the OLE IDataObject interface auto dataObj = mozilla::MakeRefPtr(aUri); // Now set it up with all the right data flavors & enums nsresult res = SetupNativeDataObject(aTransferable, dataObj, aMightNeedToFlush); if (NS_SUCCEEDED(res)) { dataObj.forget(aDataObj); } return res; } static nsresult StoreValueInDataObject(nsDataObj* aObj, LPCWSTR aClipboardFormat, DWORD value) { ScopedOLEMemory hGlobalMemory; if (!hGlobalMemory) { return NS_ERROR_OUT_OF_MEMORY; } *hGlobalMemory.lock() = value; STGMEDIUM stg; stg.tymed = TYMED_HGLOBAL; stg.pUnkForRelease = nullptr; stg.hGlobal = hGlobalMemory.forget(); FORMATETC fe; SET_FORMATETC(fe, ::RegisterClipboardFormat(aClipboardFormat), 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL) aObj->SetData(&fe, &stg, TRUE); return NS_OK; } //------------------------------------------------------------------------- nsresult nsClipboard::SetupNativeDataObject( nsITransferable* aTransferable, IDataObject* aDataObj, MightNeedToFlush* aMightNeedToFlush) { MOZ_ASSERT(aTransferable); MOZ_ASSERT(aDataObj); if (!aTransferable || !aDataObj) { return NS_ERROR_FAILURE; } auto* dObj = static_cast(aDataObj); if (aMightNeedToFlush) { *aMightNeedToFlush = MightNeedToFlush::No; } // Now give the Transferable to the DataObject // for getting the data out of it dObj->SetTransferable(aTransferable); // Get the transferable list of data flavors nsTArray flavors; aTransferable->FlavorsTransferableCanExport(flavors); // Track web custom format flavors so we can publish a single JSON map // alongside the per-clipboard-format data. The map essence (the part after // "web ") maps to the clipboard format name we registered under // "Web Custom FormatN". mozilla::widget::WebCustomFormatMap webCustomFormatMap; uint32_t webCustomFormatIndex = 0; // Walk through flavors that contain data and register them // into the DataObj as supported flavors for (uint32_t i = 0; i < flavors.Length(); i++) { nsCString& flavorStr = flavors[i]; UINT format; if (StringBeginsWith(flavorStr, nsLiteralCString(kWebCustomFormatPrefix))) { if (!nsBaseClipboard::IsValidFlavor(flavorStr)) { continue; } // Each "web foo/bar" gets its own per-slot clipboard format published // under the "Web Custom FormatN" name; the essence -> slot mapping is // recorded so the map JSON below can advertise it. nsAutoCString clipboardFormatName; clipboardFormatName.AppendLiteral("Web Custom Format"); clipboardFormatName.AppendInt(webCustomFormatIndex); format = GetFormat(clipboardFormatName.get(), false); nsDependentCSubstring essence( Substring(flavorStr, strlen(kWebCustomFormatPrefix))); webCustomFormatMap.InsertOrUpdate(essence, clipboardFormatName); webCustomFormatIndex++; } else { // When putting data onto the clipboard, we want to maintain kHTMLMime // ("text/html") and not map it to CF_HTML here since this will be done // below. format = GetFormat(flavorStr.get(), false); } // Now tell the native IDataObject about both our mime type and // the native data format FORMATETC fe; SET_FORMATETC(fe, format, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL); dObj->AddDataFlavor(flavorStr.get(), &fe); // Do various things internal to the implementation, like map one // flavor to another or add additional flavors based on what's required // for the win32 impl. if (flavorStr.EqualsLiteral(kTextMime)) { // if we find text/plain, also add CF_TEXT, but we can add it for // text/plain as well. FORMATETC textFE; SET_FORMATETC(textFE, CF_TEXT, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL); dObj->AddDataFlavor(kTextMime, &textFE); if (aMightNeedToFlush) { *aMightNeedToFlush = MightNeedToFlush::Yes; } } else if (flavorStr.EqualsLiteral(kHTMLMime)) { // if we find text/html, also advertise win32's html flavor (which we will // convert on our own in nsDataObj::GetText(). FORMATETC htmlFE; SET_FORMATETC(htmlFE, GetHtmlClipboardFormat(), 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL); dObj->AddDataFlavor(kHTMLMime, &htmlFE); } else if (flavorStr.EqualsLiteral(kURLMime)) { // if we're a url, in addition to also being text, we need to register // the "file" flavors so that the win32 shell knows to create an internet // shortcut when it sees one of these beasts. FORMATETC shortcutFE; SET_FORMATETC(shortcutFE, ::RegisterClipboardFormat(CFSTR_FILEDESCRIPTORA), 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL) dObj->AddDataFlavor(kURLMime, &shortcutFE); SET_FORMATETC(shortcutFE, ::RegisterClipboardFormat(CFSTR_FILEDESCRIPTORW), 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL) dObj->AddDataFlavor(kURLMime, &shortcutFE); SET_FORMATETC(shortcutFE, ::RegisterClipboardFormat(CFSTR_FILECONTENTS), 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL) dObj->AddDataFlavor(kURLMime, &shortcutFE); SET_FORMATETC(shortcutFE, ::RegisterClipboardFormat(CFSTR_INETURLA), 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL) dObj->AddDataFlavor(kURLMime, &shortcutFE); SET_FORMATETC(shortcutFE, ::RegisterClipboardFormat(CFSTR_INETURLW), 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL) dObj->AddDataFlavor(kURLMime, &shortcutFE); } else if (flavorStr.EqualsLiteral(kPNGImageMime) || flavorStr.EqualsLiteral(kJPEGImageMime) || flavorStr.EqualsLiteral(kJPGImageMime) || flavorStr.EqualsLiteral(kGIFImageMime) || flavorStr.EqualsLiteral(kNativeImageMime)) { // if we're an image, register the relevant bitmap flavors FORMATETC imageFE; // Add PNG, depending on prefs if (mozilla::StaticPrefs::clipboard_copy_image_as_png()) { static const CLIPFORMAT CF_PNG = ::RegisterClipboardFormat(TEXT("PNG")); SET_FORMATETC(imageFE, CF_PNG, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL) dObj->AddDataFlavor(flavorStr.get(), &imageFE); } // Add DIBv5 SET_FORMATETC(imageFE, CF_DIBV5, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL) dObj->AddDataFlavor(flavorStr.get(), &imageFE); // Add DIBv3 SET_FORMATETC(imageFE, CF_DIB, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL) dObj->AddDataFlavor(flavorStr.get(), &imageFE); } else if (flavorStr.EqualsLiteral(kFilePromiseMime)) { // if we're a file promise flavor, also register the // CFSTR_PREFERREDDROPEFFECT format. The data object // returns a value of DROPEFFECTS_MOVE to the drop target // when it asks for the value of this format. This causes // the file to be moved from the temporary location instead // of being copied. The right thing to do here is to call // SetData() on the data object and set the value of this format // to DROPEFFECTS_MOVE on this particular data object. But, // since all the other clipboard formats follow the model of setting // data on the data object only when the drop object calls GetData(), // I am leaving this format's value hard coded in the data object. // We can change this if other consumers of this format get added to this // codebase and they need different values. FORMATETC shortcutFE; SET_FORMATETC(shortcutFE, ::RegisterClipboardFormat(CFSTR_PREFERREDDROPEFFECT), 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL) dObj->AddDataFlavor(kFilePromiseMime, &shortcutFE); } } if (!webCustomFormatMap.IsEmpty()) { // Publish the web-custom-format map alongside the per-slot payloads. // The JSON is held on the data object so nsDataObj::GetText can serve // it without consulting the transferable. nsAutoCString mapJson; mozilla::widget::WebCustomFormatMapToJSON(webCustomFormatMap, mapJson); dObj->SetWebCustomFormatMapJson(mapJson); FORMATETC mapFE; SET_FORMATETC(mapFE, GetFormat(kWebCustomFormatMapType, false), 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL); dObj->AddDataFlavor(kWebCustomFormatMapType, &mapFE); } if (!mozilla::StaticPrefs:: clipboard_copyPrivateDataToClipboardCloudOrHistory()) { // Let Clipboard know that data is sensitive and must not be copied to // the Cloud Clipboard, Clipboard History and similar. // https://docs.microsoft.com/en-us/windows/win32/dataxchg/clipboard-formats#cloud-clipboard-and-clipboard-history-formats if (aTransferable->GetIsPrivateData()) { nsresult rv = StoreValueInDataObject(dObj, TEXT("CanUploadToCloudClipboard"), 0); NS_ENSURE_SUCCESS(rv, rv); rv = StoreValueInDataObject(dObj, TEXT("CanIncludeInClipboardHistory"), 0); NS_ENSURE_SUCCESS(rv, rv); rv = StoreValueInDataObject( dObj, TEXT("ExcludeClipboardContentFromMonitorProcessing"), 0); NS_ENSURE_SUCCESS(rv, rv); } } return NS_OK; } // See methods listed at // . static void IDataObjectMethodResultToString(const HRESULT aHres, nsACString& aResult) { switch (aHres) { case E_INVALIDARG: aResult = "E_INVALIDARG"; break; case E_UNEXPECTED: aResult = "E_UNEXPECTED"; break; case E_OUTOFMEMORY: aResult = "E_OUTOFMEMORY"; break; case DV_E_LINDEX: aResult = "DV_E_LINDEX"; break; case DV_E_FORMATETC: aResult = "DV_E_FORMATETC"; break; case DV_E_TYMED: aResult = "DV_E_TYMED"; break; case DV_E_DVASPECT: aResult = "DV_E_DVASPECT"; break; case OLE_E_NOTRUNNING: aResult = "OLE_E_NOTRUNNING"; break; case STG_E_MEDIUMFULL: aResult = "STG_E_MEDIUMFULL"; break; case DV_E_CLIPFORMAT: aResult = "DV_E_CLIPFORMAT"; break; case S_OK: aResult = "S_OK"; break; default: // Explicit template instantiaton, because otherwise the call is // ambiguous. constexpr int kRadix = 16; aResult = IntToCString(aHres, kRadix); break; } } // See // . static void OleGetClipboardResultToString(const HRESULT aHres, nsACString& aResult) { switch (aHres) { case S_OK: aResult = "S_OK"; break; case CLIPBRD_E_CANT_OPEN: aResult = "CLIPBRD_E_CANT_OPEN"; break; case CLIPBRD_E_CANT_CLOSE: aResult = "CLIPBRD_E_CANT_CLOSE"; break; default: // Explicit template instantiaton, because otherwise the call is // ambiguous. constexpr int kRadix = 16; aResult = IntToCString(aHres, kRadix); break; } } static void MaybeLogClipboardCurrentOwner( const HRESULT aHres, const mozilla::StaticString& aMethodName) { if (!MOZ_CLIPBOARD_LOG_ENABLED()) { return; } if (aHres != CLIPBRD_E_CANT_OPEN) { return; } auto hwnd = ::GetOpenClipboardWindow(); if (!hwnd) { MOZ_CLIPBOARD_LOG( "IDataObject::%s | Clipboard already opened by unknown process", aMethodName.get()); return; } DWORD procId; DWORD threadId = ::GetWindowThreadProcessId(hwnd, &procId); NS_ENSURE_TRUE_VOID(threadId); nsAutoString procName; NS_ENSURE_SUCCESS_VOID( mozilla::widget::WinUtils::GetProcessImageName(procId, procName)); MOZ_CLIPBOARD_LOG( "IDataObject::%s | Clipboard already opened by HWND: %p | " "Process ID: %lu | Thread ID: %lu | App name: %s", aMethodName.get(), hwnd, procId, threadId, NS_ConvertUTF16toUTF8(procName).get()); } // See // . static void LogOleGetClipboardResult(const HRESULT aHres) { if (MOZ_CLIPBOARD_LOG_ENABLED()) { nsAutoCString hresString; OleGetClipboardResultToString(aHres, hresString); MOZ_CLIPBOARD_LOG("OleGetClipboard result: %s", hresString.get()); MaybeLogClipboardCurrentOwner(aHres, "OleGetClipboard"); } } // See // . static void OleSetClipboardResultToString(HRESULT aHres, nsACString& aResult) { switch (aHres) { case S_OK: aResult = "S_OK"; break; case CLIPBRD_E_CANT_OPEN: aResult = "CLIPBRD_E_CANT_OPEN"; break; case CLIPBRD_E_CANT_EMPTY: aResult = "CLIPBRD_E_CANT_EMPTY"; break; case CLIPBRD_E_CANT_CLOSE: aResult = "CLIPBRD_E_CANT_CLOSE"; break; case CLIPBRD_E_CANT_SET: aResult = "CLIPBRD_E_CANT_SET"; break; default: // Explicit template instantiaton, because otherwise the call is // ambiguous. constexpr int kRadix = 16; aResult = IntToCString(aHres, kRadix); break; } } // See // . static void LogOleSetClipboardResult(const HRESULT aHres) { if (MOZ_CLIPBOARD_LOG_ENABLED()) { nsAutoCString hresString; OleSetClipboardResultToString(aHres, hresString); MOZ_CLIPBOARD_LOG("OleSetClipboard result: %s", hresString.get()); MaybeLogClipboardCurrentOwner(aHres, "OleSetClipboard"); } } template static HRESULT RepeatedlyTry(Function aFunction, LogFunction aLogFunction, Args... aArgs) { // These are magic values based on local testing. They are chosen not higher // to avoid jank (). // When changing them, be careful. static constexpr int kNumberOfTries = 3; static constexpr int kDelayInMs = 3; HRESULT hres; for (int i = 0; i < kNumberOfTries; ++i) { hres = aFunction(aArgs...); aLogFunction(hres); if (hres == S_OK) { break; } // TODO: This was formerly std::sleep_for, which wasn't actually sleeping // in tests (bug 1927664). ::SleepEx(kDelayInMs, TRUE); } return hres; } // Other apps can block access to the clipboard. This repeatedly // calls `::OleSetClipboard` for a fixed number of times and should be called // instead of `::OleSetClipboard`. static void RepeatedlyTryOleSetClipboard(IDataObject* aDataObj) { RepeatedlyTry(::OleSetClipboard, LogOleSetClipboardResult, aDataObj); } //------------------------------------------------------------------------- NS_IMETHODIMP nsClipboard::SetNativeClipboardData( nsITransferable* aTransferable, ClipboardType aWhichClipboard) { MOZ_CLIPBOARD_LOG("%s", __FUNCTION__); if (aWhichClipboard != kGlobalClipboard) { return NS_ERROR_FAILURE; } // make sure we have a good transferable if (!aTransferable) { return NS_ERROR_FAILURE; } #ifdef ACCESSIBILITY mozilla::a11y::Compatibility::SuppressA11yForClipboardCopy(); #endif RefPtr dataObj; auto mightNeedToFlush = MightNeedToFlush::No; if (NS_SUCCEEDED(CreateNativeDataObject(aTransferable, getter_AddRefs(dataObj), nullptr, &mightNeedToFlush))) { RepeatedlyTryOleSetClipboard(dataObj); const bool doFlush = [&] { switch (mozilla::StaticPrefs::widget_windows_sync_clipboard_flush()) { case 0: return false; case 1: return true; default: // Bug 1774285: Windows Suggested Actions (introduced in Windows 11 // 22H2) walks the entire a11y tree using UIA if something is placed // on the clipboard using delayed rendering. (The OLE clipboard always // uses delayed rendering.) This a11y tree walk causes an unacceptable // hang, particularly when the a11y cache is disabled. We choose the // lesser of the two performance/memory evils here and force immediate // rendering as part of our workaround. return mightNeedToFlush == MightNeedToFlush::Yes && mozilla::IsWin1122H2OrLater(); } }(); if (doFlush) { RepeatedlyTry(::OleFlushClipboard, [](HRESULT) {}); } } else { // Clear the native clipboard RepeatedlyTryOleSetClipboard(nullptr); } return NS_OK; } //------------------------------------------------------------------------- nsresult nsClipboard::GetGlobalData(HGLOBAL aHGBL, void** aData, uint32_t* aLen) { MOZ_CLIPBOARD_LOG("%s", __FUNCTION__); // Allocate a new memory buffer and copy the data from global memory. // // Some callers of this function call `NS_strlen(char16_t*)` on the returned // data buffer -- even though there's no guarantee that the data is a wide // string, let alone NUL-terminated. As a safety precaution, allocate a // slightly longer buffer than necessary, and append three bytes' worth of // NUL. // // (These bytes are not reported in *aLen, so callers which sensibly use that // as a limit will not need to worry about stray trailing bytes.) if (aHGBL != nullptr) { ScopedOLELock lpStr(aHGBL); mozilla::CheckedInt allocSize = mozilla::CheckedInt(lpStr.size()) + 3; if (!allocSize.isValid()) { return NS_ERROR_INVALID_ARG; } char* data = static_cast(malloc(allocSize.value())); if (!data) return NS_ERROR_FAILURE; std::copy(lpStr.begin(), lpStr.end(), data); // null terminate for safety std::fill_n(data + lpStr.size(), 3, '\0'); *aData = data; *aLen = lpStr.size(); return NS_OK; } // We really shouldn't ever get here // but just in case *aData = nullptr; *aLen = 0; LPVOID lpMsgBuf; ::FormatMessageW( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, nullptr, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Default language (LPWSTR)&lpMsgBuf, 0, nullptr); // Display the string. ::MessageBoxW(nullptr, (LPCWSTR)lpMsgBuf, L"GetLastError", MB_OK | MB_ICONINFORMATION); // Free the buffer. ::LocalFree(lpMsgBuf); return NS_ERROR_FAILURE; } //------------------------------------------------------------------------- nsresult nsClipboard::GetNativeDataOffClipboard(nsIWidget* aWidget, UINT /*aIndex*/, UINT aFormat, void** aData, uint32_t* aLen, uint64_t aThreshold) { MOZ_CLIPBOARD_LOG("%s: overload taking nsIWidget*.", __FUNCTION__); HGLOBAL hglb; nsresult result = NS_ERROR_FAILURE; HWND nativeWin = nullptr; if (::OpenClipboard(nativeWin)) { hglb = ::GetClipboardData(aFormat); if (aThreshold) { if (aFormat == CF_TEXT) { MOZ_TRY(CheckClipboardByteSize(hglb, aThreshold + 1)); } else if (aFormat == CF_UNICODETEXT) { MOZ_TRY(CheckClipboardByteSize(hglb, aThreshold + sizeof(wchar_t))); } } result = GetGlobalData(hglb, aData, aLen); ::CloseClipboard(); } return result; } // See methods listed at // . static void LogIDataObjectMethodResult(const HRESULT aHres, mozilla::StaticString aMethodName) { if (MOZ_CLIPBOARD_LOG_ENABLED()) { nsAutoCString hresString; IDataObjectMethodResultToString(aHres, hresString); MOZ_CLIPBOARD_LOG("IDataObject::%s result : %s", aMethodName.get(), hresString.get()); MaybeLogClipboardCurrentOwner(aHres, aMethodName); } } // Other apps can block access to the clipboard. This repeatedly calls // `GetData` for a fixed number of times and should be called instead of // `GetData`. See // . // While Microsoft's documentation doesn't include `CLIPBRD_E_CANT_OPEN` // explicitly, it allows it implicitly and in local experiments it was indeed // returned. static HRESULT RepeatedlyTryGetData(IDataObject& aDataObject, LPFORMATETC pFE, LPSTGMEDIUM pSTM) { return RepeatedlyTry( [&aDataObject, &pFE, &pSTM]() { return aDataObject.GetData(pFE, pSTM); }, [](HRESULT hres) { LogIDataObjectMethodResult(hres, "GetData"); }); } //------------------------------------------------------------------------- // static HRESULT nsClipboard::FillSTGMedium(IDataObject* aDataObject, UINT aFormat, LPFORMATETC pFE, LPSTGMEDIUM pSTM, DWORD aTymed) { SET_FORMATETC(*pFE, aFormat, 0, DVASPECT_CONTENT, -1, aTymed); // Starting by querying for the data to see if we can get it as from global // memory HRESULT hres = S_FALSE; hres = aDataObject->QueryGetData(pFE); LogIDataObjectMethodResult(hres, "QueryGetData"); if (S_OK == hres) { hres = RepeatedlyTryGetData(*aDataObject, pFE, pSTM); } return hres; } //------------------------------------------------------------------------- // aMIMEFlavor lets the caller hint which transferable flavor is being // fetched. It must be a type for which we have an image encoder (e.g. // image/png) when aFormat is CF_DIBV5, and it is consulted to detect web // custom format payloads ("web foo/bar") so the trailing UTF-16 null pad is // suppressed. May be null otherwise. nsresult nsClipboard::GetNativeDataOffClipboard(IDataObject* aDataObject, UINT aIndex, UINT aFormat, const char* aMIMEFlavor, void** aData, uint32_t* aLen, uint64_t aThreshold) { MOZ_CLIPBOARD_LOG("%s: overload taking IDataObject*.", __FUNCTION__); *aData = nullptr; *aLen = 0; if (!aDataObject) { return NS_ERROR_FAILURE; } UINT const format = aFormat; FORMATETC fe; STGMEDIUM stm; HRESULT hres = FillSTGMedium(aDataObject, format, &fe, &stm, TYMED_HGLOBAL); // If the format is CF_HDROP and we haven't found any files we can try looking // for virtual files with FILEDESCRIPTOR. if (FAILED(hres) && format == CF_HDROP) { hres = FillSTGMedium(aDataObject, nsClipboard::GetClipboardFileDescriptorFormatW(), &fe, &stm, TYMED_HGLOBAL); if (FAILED(hres)) { hres = FillSTGMedium(aDataObject, nsClipboard::GetClipboardFileDescriptorFormatA(), &fe, &stm, TYMED_HGLOBAL); } } // N.B.: not `FAILED(hres)`, as this can be `S_FALSE`! if (hres != S_OK) { return NS_ERROR_FAILURE; } // otherwise, there is something in stm; make sure we delete it on exit auto const _release_stm = mozilla::MakeScopeExit([&stm] { ::ReleaseStgMedium(&stm); }); static CLIPFORMAT fileDescriptorFlavorA = ::RegisterClipboardFormat(CFSTR_FILEDESCRIPTORA); static CLIPFORMAT fileDescriptorFlavorW = ::RegisterClipboardFormat(CFSTR_FILEDESCRIPTORW); static CLIPFORMAT fileFlavor = ::RegisterClipboardFormat(CFSTR_FILECONTENTS); static CLIPFORMAT preferredDropEffect = ::RegisterClipboardFormat(CFSTR_PREFERREDDROPEFFECT); static CLIPFORMAT pngFlavor = ::RegisterClipboardFormat(TEXT("PNG")); // Historical note: when this code was first written (bug #9367, 1999-07-09), // it was believed we would need to handle other values of stm.tymed. As of // 2024-01-09, such a need has not yet materialized. if (stm.tymed != TYMED_HGLOBAL) { MOZ_CLIPBOARD_LOG("unhandled TYMED_* value: %lu", stm.tymed); return NS_ERROR_FAILURE; } // compile-time-constant format indicators: switch (fe.cfFormat) { case CF_TEXT: { if (aThreshold > 0) { MOZ_TRY(CheckClipboardByteSize(stm.hGlobal, aThreshold + 1)); } // Get the data out of the global data handle. The size we // return should not include the null because the other // platforms don't use nulls, so just return the length we get // back from strlen(), since we know CF_TEXT is null // terminated. Recall that GetGlobalData() returns the size of // the allocated buffer, not the size of the data (on 98, these // are not the same) so we can't use that. uint32_t allocLen = 0; MOZ_TRY(GetGlobalData(stm.hGlobal, aData, &allocLen)); *aLen = strlen(reinterpret_cast(*aData)); return NS_OK; } case CF_UNICODETEXT: { if (aThreshold > 0) { MOZ_TRY( CheckClipboardByteSize(stm.hGlobal, aThreshold + sizeof(wchar_t))); } // Get the data out of the global data handle. The size we // return should not include the null because the other // platforms don't use nulls, so just return the length we get // back from strlen(), since we know CF_UNICODETEXT is null // terminated. Recall that GetGlobalData() returns the size of // the allocated buffer, not the size of the data (on 98, these // are not the same) so we can't use that. uint32_t allocLen = 0; MOZ_TRY(GetGlobalData(stm.hGlobal, aData, &allocLen)); *aLen = NS_strlen(reinterpret_cast(*aData)) * 2; return NS_OK; } case CF_DIBV5: { if (!aMIMEFlavor) { return NS_ERROR_FAILURE; } uint32_t allocLen = 0; const char* clipboardData = nullptr; auto const _freeClipboardData = mozilla::MakeScopeExit([&]() { free((void*)clipboardData); }); MOZ_TRY(GetGlobalData(stm.hGlobal, (void**)&clipboardData, &allocLen)); nsCOMPtr container; nsCOMPtr imgTools = do_CreateInstance("@mozilla.org/image/tools;1"); MOZ_TRY(imgTools->DecodeImageFromBuffer( clipboardData, allocLen, nsLiteralCString(IMAGE_BMP_MS_CLIPBOARD), getter_AddRefs(container))); nsAutoCString mimeType; if (strcmp(aMIMEFlavor, kJPGImageMime) == 0) { mimeType.Assign(IMAGE_JPEG); } else { mimeType.Assign(aMIMEFlavor); } nsCOMPtr inputStream; MOZ_TRY(imgTools->EncodeImage(container, mimeType, u""_ns, getter_AddRefs(inputStream))); if (!inputStream) { return NS_ERROR_FAILURE; } *aData = inputStream.forget().take(); *aLen = sizeof(nsIInputStream*); return NS_OK; } case CF_HDROP: { // in the case of a file drop, multiple files are stashed within a // single data object. In order to match mozilla's D&D apis, we // just pull out the file at the requested index, pretending as // if there really are multiple drag items. ScopedOLELock dropFiles(stm.hGlobal); UINT numFiles = ::DragQueryFileW(dropFiles.get(), 0xFFFFFFFF, nullptr, 0); if (numFiles == 0) { NS_WARNING("CF_HDROP received with empty file list"); return NS_ERROR_FAILURE; } // Note that (partly for historical reasons) we do not consider it to be // an error on Gecko's part to request index 0 of a data object that turns // out to be empty. That case is handled above. if (aIndex >= numFiles) { MOZ_ASSERT(false, "Asked for a file index out of range of list"); return NS_ERROR_INVALID_ARG; } UINT fileNameLen = ::DragQueryFileW(dropFiles.get(), aIndex, nullptr, 0); wchar_t* buffer = reinterpret_cast( moz_xmalloc((fileNameLen + 1) * sizeof(wchar_t))); ::DragQueryFileW(dropFiles.get(), aIndex, buffer, fileNameLen + 1); *aData = buffer; *aLen = fileNameLen * sizeof(char16_t); return NS_OK; } default: /* fallthrough */; } // switch (fe.cfFormat) // non-compile-time-constant format indicators: if (fe.cfFormat == fileDescriptorFlavorA || fe.cfFormat == fileDescriptorFlavorW) { nsAutoString tempPath; if (fe.cfFormat == fileDescriptorFlavorW) { ScopedOLELock fgdesc(stm.hGlobal); if (fgdesc) { if (aIndex >= fgdesc->cItems || !FileGroupDescriptorHasItems( stm.hGlobal, static_cast(aIndex) + 1)) { return NS_ERROR_INVALID_ARG; } const WCHAR* fileName = fgdesc->fgd[aIndex].cFileName; size_t nameLen = ::wcsnlen(fileName, MAX_PATH); if (nameLen == MAX_PATH) { return NS_ERROR_INVALID_ARG; } MOZ_TRY( GetTempFilePath(nsDependentString(fileName, nameLen), tempPath)); } } else { ScopedOLELock fgdesc(stm.hGlobal); if (fgdesc) { if (aIndex >= fgdesc->cItems || !FileGroupDescriptorHasItems( stm.hGlobal, static_cast(aIndex) + 1)) { return NS_ERROR_INVALID_ARG; } const CHAR* fileName = fgdesc->fgd[aIndex].cFileName; size_t nameLen = ::strnlen(fileName, MAX_PATH); if (nameLen == MAX_PATH) { return NS_ERROR_INVALID_ARG; } nsAutoString wideName; MOZ_TRY(NS_CopyNativeToUnicode(nsDependentCSubstring(fileName, nameLen), wideName)); MOZ_TRY(GetTempFilePath(wideName, tempPath)); } } MOZ_TRY(SaveStorageOrStream(aDataObject, aIndex, tempPath)); wchar_t* buffer = reinterpret_cast( moz_xmalloc((tempPath.Length() + 1) * sizeof(wchar_t))); wcscpy(buffer, tempPath.get()); *aData = buffer; *aLen = tempPath.Length() * sizeof(wchar_t); return NS_OK; } if (fe.cfFormat == pngFlavor) { MOZ_ASSERT(!strcmp(aMIMEFlavor, kPNGImageMime)); uint32_t allocLen = 0; const char* clipboardData = nullptr; auto const _freeClipboardData = mozilla::MakeScopeExit([&]() { free((void*)clipboardData); }); MOZ_TRY(GetGlobalData(stm.hGlobal, (void**)&clipboardData, &allocLen)); nsCOMPtr container; nsCOMPtr imgTools = do_CreateInstance("@mozilla.org/image/tools;1"); MOZ_TRY(imgTools->DecodeImageFromBuffer(clipboardData, allocLen, nsLiteralCString(kPNGImageMime), getter_AddRefs(container))); nsCOMPtr inputStream; MOZ_TRY(imgTools->EncodeImage(container, nsLiteralCString(kPNGImageMime), u""_ns, getter_AddRefs(inputStream))); if (!inputStream) { return NS_ERROR_FAILURE; } *aData = inputStream.forget().take(); *aLen = sizeof(nsIInputStream*); return NS_OK; } if (fe.cfFormat == fileFlavor) { NS_WARNING( "Mozilla doesn't yet understand how to read this type of " "file flavor"); return NS_ERROR_FAILURE; } // Get the data out of the global data handle. The size we // return should not include the null because the other // platforms don't use nulls, so just return the length we get // back from strlen(), since we know CF_UNICODETEXT is null // terminated. Recall that GetGlobalData() returns the size of // the allocated buffer, not the size of the data (on 98, these // are not the same) so we can't use that. // // NOTE: we are assuming that anything that falls into this // default case is unicode. As we start to get more // kinds of binary data, this may become an incorrect // assumption. Stay tuned. uint32_t allocLen = 0; MOZ_TRY(GetGlobalData(stm.hGlobal, aData, &allocLen)); if (fe.cfFormat == GetHtmlClipboardFormat()) { // CF_HTML is actually UTF8, not unicode, so disregard the // assumption above. We have to check the header for the // actual length, and we'll do that in FindPlatformHTML(). // For now, return the allocLen. This case is mostly to // ensure we don't try to call strlen on the buffer. *aLen = allocLen; } else if (fe.cfFormat == GetCustomClipboardFormat() || fe.cfFormat == GetWebCustomFormatMapClipboardFormat() || (aMIMEFlavor && !strncmp(aMIMEFlavor, kWebCustomFormatPrefix, strlen(kWebCustomFormatPrefix)))) { // Binary data: legacy application/x-moz-custom-clipdata, the web custom // format map (JSON, no terminator), or one of the "Web Custom FormatN" // clipboard formats backing a web custom format payload. The writer // stores the exact byte length without a trailing terminator, so we // mustn't fall through to the default UTF-16 strlen interpretation // (which would over-read into the zero-padded HGLOBAL tail). *aLen = allocLen; } else if (fe.cfFormat == preferredDropEffect) { // As per the MSDN doc entitled: "Shell Clipboard Formats" // CFSTR_PREFERREDDROPEFFECT should return a DWORD // Reference: // http://msdn.microsoft.com/en-us/library/bb776902(v=vs.85).aspx NS_ASSERTION(allocLen == sizeof(DWORD), "CFSTR_PREFERREDDROPEFFECT should return a DWORD"); *aLen = allocLen; } else { *aLen = NS_strlen(reinterpret_cast(*aData)) * sizeof(char16_t); } return NS_OK; } //------------------------------------------------------------------------- mozilla::Result, nsresult> nsClipboard::GetDataFromDataObject(IDataObject* aDataObject, UINT anIndex, nsIWidget* aWindow, const nsCString& aFlavor, uint64_t aThreshold) { MOZ_CLIPBOARD_LOG("%s", __FUNCTION__); // kWebCustomFormatMapType is a synthetic flavor: its payload isn't on the // clipboard as bytes-and-flavor; it's the "Web Custom Format Map" JSON. Use // the helper to fetch and parse it, then surface the per-essence flavors // as an nsIMutableArray. The rest of GetDataFromDataObject doesn't apply // to this case. if (aFlavor.EqualsLiteral(kWebCustomFormatMapType)) { mozilla::widget::WebCustomFormatMap map; if (!GetWebCustomFormatMapFromClipboard(aDataObject, aWindow, map)) { return nsCOMPtr{}; } nsCOMPtr customFormats = do_CreateInstance(NS_ARRAY_CONTRACTID); for (const auto& essence : map.Keys()) { nsCOMPtr customFormat = do_CreateInstance(NS_SUPPORTS_CSTRING_CONTRACTID); customFormat->SetData(nsLiteralCString(kWebCustomFormatPrefix) + essence); customFormats->AppendElement(customFormat); } return nsCOMPtr(std::move(customFormats)); } // Resolve the Windows clipboard format for aFlavor. Standard flavors go // through GetFormat(); a "web foo/bar" flavor is dynamic and resolved via // a lookup in the published "Web Custom Format Map" JSON. const bool isWebFormat = StringBeginsWith(aFlavor, nsLiteralCString(kWebCustomFormatPrefix)); UINT format; if (isWebFormat) { mozilla::widget::WebCustomFormatMap map; if (!GetWebCustomFormatMapFromClipboard(aDataObject, aWindow, map)) { return nsCOMPtr{}; } nsDependentCSubstring essence( Substring(aFlavor, strlen(kWebCustomFormatPrefix))); auto entry = map.Lookup(essence); if (!entry) { return nsCOMPtr{}; } format = GetFormat(entry.Data().get()); if (!format) { return nsCOMPtr{}; } } else { format = GetFormat(aFlavor.get()); } // Try to get the data using the desired flavor. This might fail, but all is // not lost. void* data = nullptr; uint32_t dataLen = 0; bool dataFound = false; if (nullptr != aDataObject) { nsresult rv = GetNativeDataOffClipboard(aDataObject, anIndex, format, aFlavor.get(), &data, &dataLen, aThreshold); if (rv == NS_ERROR_CLIPBOARD_TOO_BIG) { return mozilla::Err(NS_ERROR_CLIPBOARD_TOO_BIG); } dataFound = NS_SUCCEEDED(rv); } else if (nullptr != aWindow) { nsresult rv = GetNativeDataOffClipboard(aWindow, anIndex, format, &data, &dataLen, aThreshold); if (rv == NS_ERROR_CLIPBOARD_TOO_BIG) { return mozilla::Err(NS_ERROR_CLIPBOARD_TOO_BIG); } dataFound = NS_SUCCEEDED(rv); } // This is our second chance to try to find some data, having not found it // when directly asking for the flavor. Let's try digging around in other // flavors to help satisfy our craving for data. if (!dataFound) { if (aFlavor.EqualsLiteral(kTextMime)) { dataFound = FindUnicodeFromPlainText(aDataObject, anIndex, &data, &dataLen); } else if (aFlavor.EqualsLiteral(kURLMime)) { // drags from other windows apps expose the native // CFSTR_INETURL{A,W} flavor dataFound = FindURLFromNativeURL(aDataObject, anIndex, &data, &dataLen); if (!dataFound) { dataFound = FindURLFromLocalFile(aDataObject, anIndex, &data, &dataLen); } } else { mozilla::Maybe secondaryFormat = GetSecondaryFormat(aFlavor.get()); if (secondaryFormat) { // Fall back to secondary format dataFound = NS_SUCCEEDED(GetNativeDataOffClipboard( aDataObject, anIndex, secondaryFormat.value(), aFlavor.get(), &data, &dataLen)); } } } // if we try one last ditch effort to find our data if (!dataFound) { return nsCOMPtr{}; } // Hopefully by this point we've found it and can go about our business nsCOMPtr genericDataWrapper; if (aFlavor.EqualsLiteral(kFileMime)) { // we have a file path in |data|. Create an nsLocalFile object. nsDependentString filepath(reinterpret_cast(data)); nsCOMPtr file; if (NS_SUCCEEDED(NS_NewLocalFile(filepath, getter_AddRefs(file)))) { genericDataWrapper = do_QueryInterface(file); } free(data); } else if (aFlavor.EqualsLiteral(kNativeHTMLMime)) { uint32_t dummy; // the editor folks want CF_HTML exactly as it's on the clipboard, no // conversions, no fancy stuff. Pull it off the clipboard, stuff it into // a wrapper and hand it back to them. if (FindPlatformHTML(aDataObject, anIndex, &data, &dummy, &dataLen)) { nsPrimitiveHelpers::CreatePrimitiveForData( aFlavor, data, dataLen, getter_AddRefs(genericDataWrapper)); } free(data); } else if (aFlavor.EqualsLiteral(kHTMLMime)) { uint32_t startOfData = 0; // The JS folks want CF_HTML exactly as it is on the clipboard, but // minus the CF_HTML header index information. // It also needs to be converted to UTF16 and have linebreaks changed. if (FindPlatformHTML(aDataObject, anIndex, &data, &startOfData, &dataLen)) { dataLen -= startOfData; nsPrimitiveHelpers::CreatePrimitiveForCFHTML( static_cast(data) + startOfData, &dataLen, getter_AddRefs(genericDataWrapper)); } free(data); } else if (aFlavor.EqualsLiteral(kJPEGImageMime) || aFlavor.EqualsLiteral(kJPGImageMime) || aFlavor.EqualsLiteral(kPNGImageMime)) { nsIInputStream* imageStream = reinterpret_cast(data); genericDataWrapper = do_QueryInterface(imageStream); NS_IF_RELEASE(imageStream); } else { // Treat custom types and web custom format payloads as raw bytes // (nsISupportsCString); CreatePrimitiveForData below picks that up // from the flavor. Skip the Win32 -> DOM linebreak conversion for // these so the bytes survive verbatim. if (!aFlavor.EqualsLiteral(kCustomTypesMime) && !isWebFormat) { bool isRTF = aFlavor.EqualsLiteral(kRTFMime); // we probably have some form of text. The DOM only wants LF, so // convert from Win32 line endings to DOM line endings. int32_t signedLen = static_cast(dataLen); nsLinebreakHelpers::ConvertPlatformToDOMLinebreaks(isRTF, &data, &signedLen); dataLen = signedLen; if (isRTF) { // RTF on Windows is known to sometimes deliver an extra null byte. if (dataLen > 0 && static_cast(data)[dataLen - 1] == '\0') { dataLen--; } } } nsPrimitiveHelpers::CreatePrimitiveForData( aFlavor, data, dataLen, getter_AddRefs(genericDataWrapper)); free(data); } return std::move(genericDataWrapper); } nsresult nsClipboard::GetDataFromDataObject(IDataObject* aDataObject, UINT anIndex, nsIWidget* aWindow, nsITransferable* aTransferable, uint64_t aThreshold) { MOZ_CLIPBOARD_LOG("%s", __FUNCTION__); // make sure we have a good transferable if (!aTransferable) { return NS_ERROR_INVALID_ARG; } // get flavor list that includes all flavors that can be written (including // ones obtained through conversion) nsTArray flavors; nsresult res = aTransferable->FlavorsTransferableCanImport(flavors); if (NS_FAILED(res)) { return NS_ERROR_FAILURE; } // Walk through flavors and see which flavor is on the clipboard them on the // native clipboard, for (uint32_t i = 0; i < flavors.Length(); i++) { const nsCString& flavorStr = flavors[i]; auto dataOrError = GetDataFromDataObject(aDataObject, anIndex, aWindow, flavorStr, aThreshold); if (dataOrError.isErr() || !dataOrError.inspect()) { continue; } NS_ASSERTION(dataOrError.inspect(), "About to put null data into the transferable"); aTransferable->SetTransferData(flavorStr.get(), dataOrError.inspect()); // we found one, get out of the loop break; } // foreach flavor return NS_OK; } // // FindPlatformHTML // // Someone asked for the OS CF_HTML flavor. We give it back to them exactly // as-is. // bool nsClipboard ::FindPlatformHTML(IDataObject* inDataObject, UINT inIndex, void** outData, uint32_t* outStartOfData, uint32_t* outDataLen) { // Reference: MSDN doc entitled "HTML Clipboard Format" // http://msdn.microsoft.com/en-us/library/aa767917(VS.85).aspx#unknown_854 // CF_HTML is UTF8, not unicode. We also can't rely on it being // null-terminated so we have to check the CF_HTML header for the correct // length. The length we return is the bytecount from the beginning of the // selected data to the end of the selected data, without the null // termination. Because it's UTF8, we're guaranteed the header is ASCII. if (!outData || !*outData) { return false; } char version[8] = {0}; int32_t startOfData = 0; int32_t endOfData = 0; int numFound = sscanf((char*)*outData, "Version:%7s\nStartHTML:%d\nEndHTML:%d", version, &startOfData, &endOfData); if (numFound != 3 || startOfData < -1 || endOfData < -1) { return false; } // Fixup the start and end markers if they have no context (set to -1) if (startOfData == -1) { startOfData = 0; } if (endOfData == -1) { endOfData = *outDataLen; } // Make sure we were passed sane values within our buffer size. // (Note that we've handled all cases of negative endOfData above, so we can // safely cast it to be unsigned here.) if (!endOfData || startOfData >= endOfData || static_cast(endOfData) > *outDataLen) { return false; } // We want to return the buffer not offset by startOfData because it will be // parsed out later (probably by HTMLEditor::ParseCFHTML) when it is still // in CF_HTML format. // We return the byte offset from the start of the data buffer to where the // HTML data starts. The caller might want to extract the HTML only. *outStartOfData = startOfData; *outDataLen = endOfData; return true; } // // FindUnicodeFromPlainText // // Looks for CF_TEXT on the clipboard and converts it into an UTF-16 string // if present. Returns this string in outData, and its length in outDataLen. // XXXndeakin Windows converts between CF_UNICODE and CF_TEXT automatically // so it doesn't seem like this is actually needed. // bool nsClipboard ::FindUnicodeFromPlainText(IDataObject* inDataObject, UINT inIndex, void** outData, uint32_t* outDataLen) { MOZ_CLIPBOARD_LOG("%s", __FUNCTION__); // We are looking for text/plain and we failed to find it on the clipboard // first, so try again with CF_TEXT. If that is present, convert it to // unicode. nsresult rv = GetNativeDataOffClipboard(inDataObject, inIndex, CF_TEXT, nullptr, outData, outDataLen); if (NS_FAILED(rv) || !*outData) { return false; } const char* castedText = static_cast(*outData); nsAutoString tmp; rv = NS_CopyNativeToUnicode(nsDependentCSubstring(castedText, *outDataLen), tmp); if (NS_FAILED(rv)) { return false; } // out with the old, in with the new free(*outData); *outData = ToNewUnicode(tmp); *outDataLen = tmp.Length() * sizeof(char16_t); return true; } // FindUnicodeFromPlainText // // FindURLFromLocalFile // // we are looking for a URL and couldn't find it, try again with looking for // a local file. If we have one, it may either be a normal file or an internet // shortcut. In both cases, however, we can get a URL (it will be a file:// url // in the local file case). // bool nsClipboard ::FindURLFromLocalFile(IDataObject* inDataObject, UINT inIndex, void** outData, uint32_t* outDataLen) { MOZ_CLIPBOARD_LOG("%s", __FUNCTION__); bool dataFound = false; nsresult loadResult = GetNativeDataOffClipboard(inDataObject, inIndex, GetFormat(kFileMime), nullptr, outData, outDataLen); if (NS_SUCCEEDED(loadResult) && *outData) { // we have a file path in |data|. Is it an internet shortcut or a normal // file? const nsDependentString filepath(static_cast(*outData)); nsCOMPtr file; nsresult rv = NS_NewLocalFile(filepath, getter_AddRefs(file)); if (NS_FAILED(rv)) { free(*outData); return dataFound; } if (IsInternetShortcut(filepath)) { free(*outData); nsAutoCString url; ResolveShortcut(file, url); if (!url.IsEmpty()) { // convert it to unicode and pass it out NS_ConvertUTF8toUTF16 urlString(url); // the internal mozilla URL format, text/x-moz-url, contains // URL\ntitle. We can guess the title from the file's name. nsAutoString title; file->GetLeafName(title); // We rely on IsInternetShortcut check that file has a .url extension. title.SetLength(title.Length() - 4); if (title.IsEmpty()) { title = urlString; } *outData = ToNewUnicode(urlString + u"\n"_ns + title); *outDataLen = NS_strlen(static_cast(*outData)) * sizeof(char16_t); dataFound = true; } } else { // we have a normal file, use some Necko objects to get our file path nsAutoCString urlSpec; NS_GetURLSpecFromFile(file, urlSpec); // convert it to unicode and pass it out free(*outData); *outData = UTF8ToNewUnicode(urlSpec); *outDataLen = NS_strlen(static_cast(*outData)) * sizeof(char16_t); dataFound = true; } // else regular file } return dataFound; } // FindURLFromLocalFile // // FindURLFromNativeURL // // we are looking for a URL and couldn't find it using our internal // URL flavor, so look for it using the native URL flavor, // CF_INETURLSTRW (We don't handle CF_INETURLSTRA currently) // bool nsClipboard ::FindURLFromNativeURL(IDataObject* inDataObject, UINT inIndex, void** outData, uint32_t* outDataLen) { MOZ_CLIPBOARD_LOG("%s", __FUNCTION__); bool dataFound = false; void* tempOutData = nullptr; uint32_t tempDataLen = 0; nsresult loadResult = GetNativeDataOffClipboard( inDataObject, inIndex, ::RegisterClipboardFormat(CFSTR_INETURLW), nullptr, &tempOutData, &tempDataLen); if (NS_SUCCEEDED(loadResult) && tempOutData) { nsDependentString urlString(static_cast(tempOutData)); // the internal mozilla URL format, text/x-moz-url, contains // URL\ntitle. Since we don't actually have a title here, // just repeat the URL to fake it. *outData = ToNewUnicode(urlString + u"\n"_ns + urlString); *outDataLen = NS_strlen(static_cast(*outData)) * sizeof(char16_t); free(tempOutData); dataFound = true; } else { loadResult = GetNativeDataOffClipboard( inDataObject, inIndex, ::RegisterClipboardFormat(CFSTR_INETURLA), nullptr, &tempOutData, &tempDataLen); if (NS_SUCCEEDED(loadResult) && tempOutData) { // CFSTR_INETURLA is (currently) equal to CFSTR_SHELLURL which is equal to // CF_TEXT which is by definition ANSI encoded. nsCString urlUnescapedA; bool unescaped = NS_UnescapeURL(static_cast(tempOutData), tempDataLen, esc_OnlyNonASCII | esc_SkipControl, urlUnescapedA); nsString urlString; if (unescaped) { NS_CopyNativeToUnicode(urlUnescapedA, urlString); } else { NS_CopyNativeToUnicode( nsDependentCString(static_cast(tempOutData), tempDataLen), urlString); } // the internal mozilla URL format, text/x-moz-url, contains // URL\ntitle. Since we don't actually have a title here, // just repeat the URL to fake it. *outData = ToNewUnicode(urlString + u"\n"_ns + urlString); *outDataLen = NS_strlen(static_cast(*outData)) * sizeof(char16_t); free(tempOutData); dataFound = true; } } return dataFound; } // FindURLFromNativeURL // Other apps can block access to the clipboard. This repeatedly // calls `::OleGetClipboard` for a fixed number of times and should be called // instead of `::OleGetClipboard`. static HRESULT RepeatedlyTryOleGetClipboard(IDataObject** aDataObj) { return RepeatedlyTry(::OleGetClipboard, LogOleGetClipboardResult, aDataObj); } // // ResolveShortcut // void nsClipboard ::ResolveShortcut(nsIFile* aFile, nsACString& outURL) { nsCOMPtr fph; nsresult rv = NS_GetFileProtocolHandler(getter_AddRefs(fph)); if (NS_FAILED(rv)) { return; } nsCOMPtr uri; rv = fph->ReadURLFile(aFile, getter_AddRefs(uri)); if (NS_FAILED(rv)) { return; } uri->GetSpec(outURL); } // ResolveShortcut // // IsInternetShortcut // // A file is an Internet Shortcut if it ends with .URL // bool nsClipboard ::IsInternetShortcut(const nsAString& inFileName) { return StringEndsWith(inFileName, u".url"_ns, nsCaseInsensitiveStringComparator); } // IsInternetShortcut //------------------------------------------------------------------------- mozilla::Result, nsresult> nsClipboard::GetNativeClipboardData(const nsACString& aFlavor, ClipboardType aWhichClipboard, uint64_t aThreshold) { MOZ_DIAGNOSTIC_ASSERT( nsIClipboard::IsClipboardTypeSupported(aWhichClipboard)); MOZ_DIAGNOSTIC_ASSERT(IsValidFlavor(aFlavor)); MOZ_CLIPBOARD_LOG("%s aWhichClipboard=%i", __FUNCTION__, aWhichClipboard); // This makes sure we can use the OLE functionality for the clipboard IDataObject* dataObj; if (S_OK == RepeatedlyTryOleGetClipboard(&dataObj)) { auto dataObjRelease = mozilla::MakeScopeExit([&] { dataObj->Release(); }); // Use OLE IDataObject for clipboard operations MOZ_CLIPBOARD_LOG(" use OLE IDataObject:"); if (MOZ_CLIPBOARD_LOG_ENABLED()) { IEnumFORMATETC* pEnum = nullptr; if (S_OK == dataObj->EnumFormatEtc(DATADIR_GET, &pEnum)) { auto pEnumRelease = mozilla::MakeScopeExit([&] { pEnum->Release(); }); FORMATETC fEtc; while (S_OK == pEnum->Next(1, &fEtc, nullptr)) { nsAutoString format; mozilla::widget::WinUtils::GetClipboardFormatAsString(fEtc.cfFormat, format); MOZ_CLIPBOARD_LOG(" FORMAT %s", NS_ConvertUTF16toUTF8(format).get()); } } } return GetDataFromDataObject(dataObj, 0, nullptr, PromiseFlatCString(aFlavor), aThreshold); } // do it the old manual way return GetDataFromDataObject(nullptr, 0, mWindow, PromiseFlatCString(aFlavor), aThreshold); } nsresult nsClipboard::EmptyNativeClipboardData(ClipboardType aWhichClipboard) { MOZ_DIAGNOSTIC_ASSERT( nsIClipboard::IsClipboardTypeSupported(aWhichClipboard)); // Some programs such as ZoneAlarm monitor clipboard usage and then open the // clipboard to scan it. If we i) empty and then ii) set data, then the // 'set data' can sometimes fail with access denied becacuse another program // has the clipboard open. So to avoid this race condition for OpenClipboard // we do not empty the clipboard when we're setting it. RepeatedlyTryOleSetClipboard(nullptr); return NS_OK; } mozilla::Result nsClipboard::GetNativeClipboardSequenceNumber(ClipboardType aWhichClipboard) { MOZ_DIAGNOSTIC_ASSERT(kGlobalClipboard == aWhichClipboard); return (int32_t)::GetClipboardSequenceNumber(); } //------------------------------------------------------------------------- mozilla::Result nsClipboard::HasNativeClipboardDataMatchingFlavors( const nsTArray& aFlavorList, ClipboardType aWhichClipboard) { MOZ_DIAGNOSTIC_ASSERT( nsIClipboard::IsClipboardTypeSupported(aWhichClipboard)); // The web custom format map is loaded lazily on the first "web " query to // avoid parsing JSON when the caller is only asking about standard formats. mozilla::widget::WebCustomFormatMap webCustomFormatMap; bool didLoadWebCustomFormatMap = false; IDataObject* webMapDataObj = nullptr; auto webMapDataObjRelease = mozilla::MakeScopeExit([&] { if (webMapDataObj) { webMapDataObj->Release(); } }); for (const auto& flavor : aFlavorList) { MOZ_DIAGNOSTIC_ASSERT(IsValidFlavor(flavor)); if (StringBeginsWith(flavor, nsLiteralCString(kWebCustomFormatPrefix))) { if (!didLoadWebCustomFormatMap) { didLoadWebCustomFormatMap = true; // Try OLE first; if that fails, the helper falls back to the // window-rooted legacy Windows clipboard path. (void)RepeatedlyTryOleGetClipboard(&webMapDataObj); if (!GetWebCustomFormatMapFromClipboard(webMapDataObj, mWindow, webCustomFormatMap)) { continue; } } nsDependentCSubstring essence( Substring(flavor, strlen(kWebCustomFormatPrefix))); auto entry = webCustomFormatMap.Lookup(essence); if (!entry) { continue; } UINT cf = GetFormat(entry.Data().get()); if (cf && IsClipboardFormatAvailable(cf)) { return true; } continue; } UINT format = GetFormat(flavor.get()); if (IsClipboardFormatAvailable(format)) { return true; } mozilla::Maybe secondaryFormat = GetSecondaryFormat(flavor.get()); if (secondaryFormat && IsClipboardFormatAvailable(secondaryFormat.value())) { return true; } } return false; } //------------------------------------------------------------------------- nsresult nsClipboard::GetTempFilePath(const nsAString& aFileName, nsAString& aFilePath) { nsresult result = NS_OK; nsCOMPtr tmpFile; result = GetSpecialSystemDirectory(OS_TemporaryDirectory, getter_AddRefs(tmpFile)); NS_ENSURE_SUCCESS(result, result); result = tmpFile->Append(aFileName); NS_ENSURE_SUCCESS(result, result); result = tmpFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0660); NS_ENSURE_SUCCESS(result, result); result = tmpFile->GetPath(aFilePath); return result; } //------------------------------------------------------------------------- nsresult nsClipboard::SaveStorageOrStream(IDataObject* aDataObject, UINT aIndex, const nsAString& aFileName) { NS_ENSURE_ARG_POINTER(aDataObject); FORMATETC fe = {0}; SET_FORMATETC(fe, RegisterClipboardFormat(CFSTR_FILECONTENTS), 0, DVASPECT_CONTENT, aIndex, TYMED_ISTORAGE | TYMED_ISTREAM); STGMEDIUM stm = {0}; HRESULT hres = aDataObject->GetData(&fe, &stm); if (FAILED(hres)) { return NS_ERROR_FAILURE; } nsPromiseFlatString flatFileName(aFileName); auto releaseMediumGuard = mozilla::MakeScopeExit([&] { ReleaseStgMedium(&stm); }); // We do this check because, even though we *asked* for IStorage or IStream, // it seems that IDataObject providers can just hand us back whatever they // feel like. See Bug 1824644 for a fun example of that! if (stm.tymed != TYMED_ISTORAGE && stm.tymed != TYMED_ISTREAM) { return NS_ERROR_FAILURE; } if (stm.tymed == TYMED_ISTORAGE) { // should never happen -- but theoretically possible, given an ill-behaved // data-source if (stm.pstg == nullptr) { return NS_ERROR_FAILURE; } RefPtr file; hres = StgCreateStorageEx( flatFileName.get(), STGM_CREATE | STGM_READWRITE | STGM_SHARE_EXCLUSIVE, STGFMT_STORAGE, 0, NULL, NULL, IID_IStorage, getter_AddRefs(file)); if (FAILED(hres)) { return NS_ERROR_FAILURE; } hres = stm.pstg->CopyTo(0, NULL, NULL, file); if (FAILED(hres)) { return NS_ERROR_FAILURE; } file->Commit(STGC_DEFAULT); return NS_OK; } MOZ_ASSERT(stm.tymed == TYMED_ISTREAM); // should never happen -- but possible given an ill-behaved data-source, and // has been seen in the wild (bug 1895681) if (stm.pstm == nullptr) { return NS_ERROR_FAILURE; } HANDLE handle = CreateFile(flatFileName.get(), GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (handle == INVALID_HANDLE_VALUE) { return NS_ERROR_FAILURE; } // Don't leave a partially-written file behind if we bail out on a read or // write error below. // NB: This must initialize before/run after fileCloseGuard. auto fileDeleteGuard = mozilla::MakeScopeExit([&] { DeleteFile(flatFileName.get()); }); auto fileCloseGuard = mozilla::MakeScopeExit([&] { CloseHandle(handle); }); const ULONG bufferSize = 4096; char buffer[bufferSize] = {0}; ULONG bytesRead = 0; DWORD bytesWritten = 0; while (true) { HRESULT result = stm.pstm->Read(buffer, bufferSize, &bytesRead); if (FAILED(result)) { return NS_ERROR_FAILURE; } if (bytesRead > bufferSize) { // It obviously couldn't legitimately write more than the buffer holds. return NS_ERROR_FAILURE; } if (bytesRead == 0) { break; } if (!WriteFile(handle, buffer, static_cast(bytesRead), &bytesWritten, NULL)) { return NS_ERROR_FAILURE; } } fileDeleteGuard.release(); return NS_OK; }