This patch includes a bunch of tiny code improvements: 1. The main part of this patch is in nsDataObj::GetText, where it sizes the buffer that we add to the STGMEDIUM by the max char size for the code page, specifically for folks who have configured Windows with the experimental UTF-8 code page, where characters can be 3 bytes. (NB: An AI checked on stateful code pages. With state, the max character size can be exceeded. However, the AI reports that Windows does not allow those pages as CP_ACP. It says: "CP_ACP can only resolve to a single-byte code page, a DBCS code page, or 65001. ISO-2022-* and UTF-7 are usable as explicit code-page arguments but Windows won't install them as the system ANSI code page. Every ACP-eligible family is stateless, and all of them held the bound in the measurements above.".) 2. ScopedOLEMemory checks that the size_t allocation is within bounds. 3. Initializes STGMEDIUM (to TYMED_NULL) in places where it wasn't before. 4. Releases allocated STGMEDIUMs on error (and return an error code when appropriate). Differential Revision: https://phabricator.services.mozilla.com/D321723
2428 lines
80 KiB
C++
2428 lines
80 KiB
C++
/* 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 "nsDataObj.h"
|
|
|
|
#include <ole2.h>
|
|
#include <shlobj.h>
|
|
|
|
#include <algorithm>
|
|
|
|
#include "IEnumFE.h"
|
|
#include "WinOLELock.h"
|
|
#include "WinUtils.h"
|
|
#include "imgIEncoder.h"
|
|
#include "imgITools.h"
|
|
#include "mozilla/CheckedInt.h"
|
|
#include "mozilla/Components.h"
|
|
#include "mozilla/LazyIdleThread.h"
|
|
#include "mozilla/Preferences.h"
|
|
#include "mozilla/SpinEventLoopUntil.h"
|
|
#include "mozilla/StaticPrefs_clipboard.h"
|
|
#include "mozilla/TextUtils.h"
|
|
#include "nsArrayUtils.h"
|
|
#include "nsCRT.h"
|
|
#include "nsClipboard.h"
|
|
#include "nsComponentManagerUtils.h"
|
|
#include "nsContentUtils.h"
|
|
#include "nsDirectoryServiceDefs.h"
|
|
#include "nsEscape.h"
|
|
#include "nsICookieJarSettings.h"
|
|
#include "nsIHttpChannel.h"
|
|
#include "nsIMIMEService.h"
|
|
#include "nsIObserverService.h"
|
|
#include "nsIOutputStream.h"
|
|
#include "nsIPrincipal.h"
|
|
#include "nsIStringBundle.h"
|
|
#include "nsISupportsPrimitives.h"
|
|
#include "nsITimer.h"
|
|
#include "nsITransferable.h"
|
|
#include "nsIURL.h"
|
|
#include "nsLocalFile.h"
|
|
#include "nsMimeTypes.h"
|
|
#include "nsNativeCharsetUtils.h"
|
|
#include "nsNetUtil.h"
|
|
#include "nsPrimitiveHelpers.h"
|
|
#include "nsPrintfCString.h"
|
|
#include "nsProxyRelease.h"
|
|
#include "nsReadableUtils.h"
|
|
#include "nsString.h"
|
|
#include "nsThreadUtils.h"
|
|
#include "nscore.h"
|
|
|
|
using namespace mozilla;
|
|
using namespace mozilla::glue;
|
|
using namespace mozilla::widget;
|
|
|
|
#define BFH_LENGTH 14
|
|
#define DEFAULT_THREAD_TIMEOUT_MS 30000
|
|
|
|
NS_IMPL_ISUPPORTS(nsDataObj::CStream, nsIStreamListener)
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// helper - initializes the stream
|
|
nsresult nsDataObj::CStream::Init(nsIURI* pSourceURI,
|
|
nsContentPolicyType aContentPolicyType,
|
|
nsIPrincipal* aRequestingPrincipal,
|
|
nsICookieJarSettings* aCookieJarSettings,
|
|
nsIReferrerInfo* aReferrerInfo) {
|
|
// we can not create a channel without a requestingPrincipal
|
|
if (!aRequestingPrincipal) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
nsresult rv;
|
|
rv = NS_NewChannel(getter_AddRefs(mChannel), pSourceURI, aRequestingPrincipal,
|
|
nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT,
|
|
aContentPolicyType, aCookieJarSettings,
|
|
nullptr, // PerformanceStorage
|
|
nullptr, // loadGroup
|
|
nullptr, // aCallbacks
|
|
nsIRequest::LOAD_FROM_CACHE);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
if (nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel)) {
|
|
rv = httpChannel->SetReferrerInfo(aReferrerInfo);
|
|
(void)NS_WARN_IF(NS_FAILED(rv));
|
|
}
|
|
|
|
// Do not HTTPS-Only/-First upgrade this request. If we reach this point, any
|
|
// potential upgrades should have already happened, or the URI may have
|
|
// already been exempt.
|
|
nsCOMPtr<nsILoadInfo> loadInfo = mChannel->LoadInfo();
|
|
loadInfo->SetHttpsOnlyStatus(nsILoadInfo::HTTPS_ONLY_EXEMPT);
|
|
|
|
rv = mChannel->AsyncOpen(this);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
return NS_OK;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// IUnknown's QueryInterface, nsISupport's AddRef and Release are shared by
|
|
// IUnknown and nsIStreamListener.
|
|
STDMETHODIMP nsDataObj::CStream::QueryInterface(REFIID refiid,
|
|
void** ppvResult) {
|
|
*ppvResult = nullptr;
|
|
if (IID_IUnknown == refiid || refiid == IID_IStream)
|
|
|
|
{
|
|
*ppvResult = this;
|
|
}
|
|
|
|
if (nullptr != *ppvResult) {
|
|
((LPUNKNOWN)*ppvResult)->AddRef();
|
|
return S_OK;
|
|
}
|
|
|
|
return E_NOINTERFACE;
|
|
}
|
|
|
|
// nsIStreamListener implementation
|
|
NS_IMETHODIMP
|
|
nsDataObj::CStream::OnDataAvailable(
|
|
nsIRequest* aRequest, nsIInputStream* aInputStream,
|
|
uint64_t aOffset, // offset within the stream
|
|
uint32_t aCount) // bytes available on this call
|
|
{
|
|
// If we've been asked to read zero bytes, call `Read` once, just to ensure
|
|
// any side-effects take place, and return immediately.
|
|
if (aCount == 0) {
|
|
char buffer[1] = {0};
|
|
uint32_t bytesReadByCall = 0;
|
|
nsresult rv = aInputStream->Read(buffer, 0, &bytesReadByCall);
|
|
MOZ_ASSERT(bytesReadByCall == 0);
|
|
return rv;
|
|
}
|
|
|
|
// Extend the write buffer for the incoming data.
|
|
size_t oldLength = mChannelData.Length();
|
|
char* buffer =
|
|
reinterpret_cast<char*>(mChannelData.AppendElements(aCount, fallible));
|
|
if (!buffer) {
|
|
return NS_ERROR_OUT_OF_MEMORY;
|
|
}
|
|
MOZ_ASSERT(mChannelData.Length() == (aOffset + aCount),
|
|
"stream length mismatch w/write buffer");
|
|
|
|
// Read() may not return aCount on a single call, so loop until we've
|
|
// accumulated all the data OnDataAvailable has promised.
|
|
uint32_t bytesRead = 0;
|
|
while (bytesRead < aCount) {
|
|
uint32_t bytesReadByCall = 0;
|
|
nsresult rv = aInputStream->Read(buffer + bytesRead, aCount - bytesRead,
|
|
&bytesReadByCall);
|
|
bytesRead += bytesReadByCall;
|
|
|
|
if (bytesReadByCall == 0) {
|
|
// A `bytesReadByCall` of zero indicates EOF without failure... but we
|
|
// were promised `aCount` elements and haven't gotten them. Return a
|
|
// generic failure.
|
|
rv = NS_ERROR_FAILURE;
|
|
}
|
|
|
|
if (NS_FAILED(rv)) {
|
|
// Drop any trailing uninitialized elements before erroring out.
|
|
mChannelData.RemoveElementsAt(oldLength + bytesRead, aCount - bytesRead);
|
|
return rv;
|
|
}
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP nsDataObj::CStream::OnStartRequest(nsIRequest* aRequest) {
|
|
mChannelResult = NS_OK;
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP nsDataObj::CStream::OnStopRequest(nsIRequest* aRequest,
|
|
nsresult aStatusCode) {
|
|
mChannelRead = true;
|
|
mChannelResult = aStatusCode;
|
|
return NS_OK;
|
|
}
|
|
|
|
// Pumps thread messages while waiting for the async listener operation to
|
|
// complete. Failing this call will fail the stream incall from Windows
|
|
// and cancel the operation.
|
|
// The method spins the event loop during operation and may unregister the
|
|
// CStream, so it holds a strong reference for its duration. Callers
|
|
// must consider that, upon return, the CStream may have been unregistered and
|
|
// destroyed, so they will need to hold a strong reference if they need it to
|
|
// exist after this.
|
|
nsresult nsDataObj::CStream::WaitForCompletion() {
|
|
// HttpBaseChannel::ReleaseListeners, as part of
|
|
// nsIRequestObserver::OnStopRequest, may drop its reference when we spin the
|
|
// event loop.
|
|
RefPtr<CStream> keepAliveDuringWait(this);
|
|
|
|
// We are guaranteed OnStopRequest will get called, so this should be ok.
|
|
SpinEventLoopUntil("widget:nsDataObj::CStream::WaitForCompletion"_ns,
|
|
[&]() { return mChannelRead; });
|
|
|
|
if (!mChannelData.Length()) mChannelResult = NS_ERROR_FAILURE;
|
|
|
|
return mChannelResult;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// IStream
|
|
STDMETHODIMP nsDataObj::CStreamBase::Clone(IStream** ppStream) {
|
|
return E_NOTIMPL;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
STDMETHODIMP nsDataObj::CStreamBase::Commit(DWORD dwFrags) { return E_NOTIMPL; }
|
|
|
|
//-----------------------------------------------------------------------------
|
|
STDMETHODIMP nsDataObj::CStreamBase::CopyTo(IStream* pDestStream,
|
|
ULARGE_INTEGER nBytesToCopy,
|
|
ULARGE_INTEGER* nBytesRead,
|
|
ULARGE_INTEGER* nBytesWritten) {
|
|
return E_NOTIMPL;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
STDMETHODIMP nsDataObj::CStreamBase::LockRegion(ULARGE_INTEGER nStart,
|
|
ULARGE_INTEGER nBytes,
|
|
DWORD dwFlags) {
|
|
return E_NOTIMPL;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
STDMETHODIMP nsDataObj::CStream::Read(void* pvBuffer, ULONG nBytesToRead,
|
|
ULONG* nBytesRead) {
|
|
RefPtr<CStream> keepAliveDuringRead(this);
|
|
|
|
// Wait for the write into our buffer to complete via the stream listener.
|
|
// We can't respond to this by saying "call us back later".
|
|
if (NS_FAILED(WaitForCompletion())) return E_FAIL;
|
|
|
|
// Bytes left for Windows to read out of our buffer
|
|
ULONG bytesLeft = mChannelData.Length() - mStreamRead;
|
|
// Let Windows know what we will hand back, usually this is the entire buffer
|
|
*nBytesRead = std::min(bytesLeft, nBytesToRead);
|
|
// Copy the buffer data over
|
|
memcpy(pvBuffer, ((char*)mChannelData.Elements() + mStreamRead), *nBytesRead);
|
|
// Update our bytes read tracking
|
|
mStreamRead += *nBytesRead;
|
|
return S_OK;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
STDMETHODIMP nsDataObj::CStreamBase::Revert(void) { return E_NOTIMPL; }
|
|
|
|
//-----------------------------------------------------------------------------
|
|
STDMETHODIMP nsDataObj::CStreamBase::Seek(LARGE_INTEGER nMove, DWORD dwOrigin,
|
|
ULARGE_INTEGER* nNewPos) {
|
|
if (nNewPos == nullptr) return STG_E_INVALIDPOINTER;
|
|
|
|
if (nMove.LowPart == 0 && nMove.HighPart == 0 &&
|
|
(dwOrigin == STREAM_SEEK_SET || dwOrigin == STREAM_SEEK_CUR)) {
|
|
nNewPos->LowPart = 0;
|
|
nNewPos->HighPart = 0;
|
|
return S_OK;
|
|
}
|
|
|
|
return E_NOTIMPL;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
STDMETHODIMP nsDataObj::CStreamBase::SetSize(ULARGE_INTEGER nNewSize) {
|
|
return E_NOTIMPL;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
STDMETHODIMP nsDataObj::CStream::Stat(STATSTG* statstg, DWORD dwFlags) {
|
|
if (statstg == nullptr) return STG_E_INVALIDPOINTER;
|
|
|
|
RefPtr<CStream> keepAliveDuringStat(this);
|
|
|
|
if (!mChannel || NS_FAILED(WaitForCompletion())) return E_FAIL;
|
|
|
|
memset((void*)statstg, 0, sizeof(STATSTG));
|
|
|
|
if (dwFlags != STATFLAG_NONAME) {
|
|
nsCOMPtr<nsIURI> sourceURI;
|
|
if (NS_FAILED(mChannel->GetURI(getter_AddRefs(sourceURI)))) {
|
|
return E_FAIL;
|
|
}
|
|
|
|
nsAutoCString strFileName;
|
|
nsCOMPtr<nsIURL> sourceURL = do_QueryInterface(sourceURI);
|
|
if (!sourceURL) return E_FAIL;
|
|
|
|
sourceURL->GetFileName(strFileName);
|
|
|
|
if (strFileName.IsEmpty()) return E_FAIL;
|
|
|
|
NS_UnescapeURL(strFileName);
|
|
NS_ConvertUTF8toUTF16 wideFileName(strFileName);
|
|
|
|
uint32_t nMaxNameLength = (wideFileName.Length() * 2) + 2;
|
|
void* retBuf = CoTaskMemAlloc(nMaxNameLength); // freed by caller
|
|
if (!retBuf) return STG_E_INSUFFICIENTMEMORY;
|
|
|
|
ZeroMemory(retBuf, nMaxNameLength);
|
|
memcpy(retBuf, wideFileName.get(), wideFileName.Length() * 2);
|
|
statstg->pwcsName = (LPOLESTR)retBuf;
|
|
}
|
|
|
|
SYSTEMTIME st;
|
|
|
|
statstg->type = STGTY_STREAM;
|
|
|
|
GetSystemTime(&st);
|
|
SystemTimeToFileTime((const SYSTEMTIME*)&st, (LPFILETIME)&statstg->mtime);
|
|
statstg->ctime = statstg->atime = statstg->mtime;
|
|
|
|
statstg->cbSize.QuadPart = mChannelData.Length();
|
|
statstg->grfMode = STGM_READ;
|
|
statstg->grfLocksSupported = LOCK_ONLYONCE;
|
|
statstg->clsid = CLSID_NULL;
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
STDMETHODIMP nsDataObj::CStreamBase::UnlockRegion(ULARGE_INTEGER nStart,
|
|
ULARGE_INTEGER nBytes,
|
|
DWORD dwFlags) {
|
|
return E_NOTIMPL;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
STDMETHODIMP nsDataObj::CStreamBase::Write(const void* pvBuffer,
|
|
ULONG nBytesToRead,
|
|
ULONG* nBytesRead) {
|
|
return E_NOTIMPL;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
HRESULT nsDataObj::CreateStream(IStream** outStream) {
|
|
NS_ENSURE_TRUE(outStream, E_INVALIDARG);
|
|
|
|
nsresult rv = NS_ERROR_FAILURE;
|
|
nsAutoString wideFileName;
|
|
nsCOMPtr<nsIURI> sourceURI;
|
|
HRESULT res;
|
|
|
|
res = GetDownloadDetails(getter_AddRefs(sourceURI), wideFileName);
|
|
if (FAILED(res)) return res;
|
|
|
|
nsDataObj::CStream* pStream = new nsDataObj::CStream();
|
|
NS_ENSURE_TRUE(pStream, E_OUTOFMEMORY);
|
|
|
|
pStream->AddRef();
|
|
|
|
// query the dataPrincipal from the transferable and add it to the new
|
|
// channel.
|
|
nsCOMPtr<nsIPrincipal> requestingPrincipal =
|
|
mTransferable->GetDataPrincipal();
|
|
MOZ_ASSERT(requestingPrincipal, "can not create channel without a principal");
|
|
|
|
// Note that the cookieJarSettings could be null if the data object is for the
|
|
// image copy. We will fix this in Bug 1690532.
|
|
nsCOMPtr<nsICookieJarSettings> cookieJarSettings =
|
|
mTransferable->GetCookieJarSettings();
|
|
|
|
// The referrer is optional.
|
|
nsCOMPtr<nsIReferrerInfo> referrerInfo = mTransferable->GetReferrerInfo();
|
|
|
|
nsContentPolicyType contentPolicyType = mTransferable->GetContentPolicyType();
|
|
rv = pStream->Init(sourceURI, contentPolicyType, requestingPrincipal,
|
|
cookieJarSettings, referrerInfo);
|
|
if (NS_FAILED(rv)) {
|
|
pStream->Release();
|
|
return E_FAIL;
|
|
}
|
|
*outStream = pStream;
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// AutoCloseEvent implementation
|
|
nsDataObj::AutoCloseEvent::AutoCloseEvent()
|
|
: mEvent(::CreateEventW(nullptr, TRUE, FALSE, nullptr)) {}
|
|
|
|
bool nsDataObj::AutoCloseEvent::IsInited() const { return !!mEvent; }
|
|
|
|
void nsDataObj::AutoCloseEvent::Signal() const { ::SetEvent(mEvent); }
|
|
|
|
DWORD nsDataObj::AutoCloseEvent::Wait(DWORD aMillisec) const {
|
|
return ::WaitForSingleObject(mEvent, aMillisec);
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// AutoSetEvent implementation
|
|
nsDataObj::AutoSetEvent::AutoSetEvent(NotNull<AutoCloseEvent*> aEvent)
|
|
: mEvent(aEvent) {}
|
|
|
|
nsDataObj::AutoSetEvent::~AutoSetEvent() { Signal(); }
|
|
|
|
void nsDataObj::AutoSetEvent::Signal() const { mEvent->Signal(); }
|
|
|
|
bool nsDataObj::AutoSetEvent::IsWaiting() const {
|
|
return mEvent->Wait(0) == WAIT_TIMEOUT;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// CMemStream implementation
|
|
Win32SRWLock nsDataObj::CMemStream::mLock;
|
|
|
|
//-----------------------------------------------------------------------------
|
|
nsDataObj::CMemStream::CMemStream(nsHGLOBAL aGlobalMem, uint32_t aTotalLength,
|
|
already_AddRefed<AutoCloseEvent> aEvent)
|
|
: mGlobalMem(aGlobalMem), mEvent(aEvent), mTotalLength(aTotalLength) {
|
|
::CoCreateFreeThreadedMarshaler(this, getter_AddRefs(mMarshaler));
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// IUnknown
|
|
STDMETHODIMP nsDataObj::CMemStream::QueryInterface(REFIID refiid,
|
|
void** ppvResult) {
|
|
*ppvResult = nullptr;
|
|
if (refiid == IID_IUnknown || refiid == IID_IStream ||
|
|
refiid == IID_IAgileObject) {
|
|
*ppvResult = this;
|
|
} else if (refiid == IID_IMarshal && mMarshaler) {
|
|
return mMarshaler->QueryInterface(refiid, ppvResult);
|
|
}
|
|
|
|
if (nullptr != *ppvResult) {
|
|
((LPUNKNOWN)*ppvResult)->AddRef();
|
|
return S_OK;
|
|
}
|
|
|
|
return E_NOINTERFACE;
|
|
}
|
|
|
|
void nsDataObj::CMemStream::WaitForCompletion() {
|
|
if (!mEvent) {
|
|
// We are not waiting for obtaining the icon cache.
|
|
return;
|
|
}
|
|
if (!NS_IsMainThread()) {
|
|
mEvent->Wait(INFINITE);
|
|
} else {
|
|
// We should not block the main thread.
|
|
mEvent->Signal();
|
|
}
|
|
// mEvent will always be in the signaled state here.
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// IStream
|
|
STDMETHODIMP nsDataObj::CMemStream::Read(void* pvBuffer, ULONG nBytesToRead,
|
|
ULONG* nBytesRead) {
|
|
// Wait until the event is signaled.
|
|
WaitForCompletion();
|
|
|
|
AutoExclusiveLock lock(mLock);
|
|
char* contents = reinterpret_cast<char*>(GlobalLock(mGlobalMem.get()));
|
|
if (!contents) {
|
|
return E_OUTOFMEMORY;
|
|
}
|
|
|
|
// Bytes left for Windows to read out of our buffer
|
|
ULONG bytesLeft = mTotalLength - mStreamRead;
|
|
// Let Windows know what we will hand back, usually this is the entire buffer
|
|
*nBytesRead = std::min(bytesLeft, nBytesToRead);
|
|
// Copy the buffer data over
|
|
memcpy(pvBuffer, contents + mStreamRead, *nBytesRead);
|
|
// Update our bytes read tracking
|
|
mStreamRead += *nBytesRead;
|
|
|
|
GlobalUnlock(mGlobalMem.get());
|
|
return S_OK;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
STDMETHODIMP nsDataObj::CMemStream::Stat(STATSTG* statstg, DWORD dwFlags) {
|
|
if (statstg == nullptr) return STG_E_INVALIDPOINTER;
|
|
|
|
memset((void*)statstg, 0, sizeof(STATSTG));
|
|
|
|
if (dwFlags != STATFLAG_NONAME) {
|
|
constexpr size_t kMaxNameLength = sizeof(wchar_t);
|
|
void* retBuf = CoTaskMemAlloc(kMaxNameLength); // freed by caller
|
|
if (!retBuf) return STG_E_INSUFFICIENTMEMORY;
|
|
|
|
ZeroMemory(retBuf, kMaxNameLength);
|
|
statstg->pwcsName = (LPOLESTR)retBuf;
|
|
}
|
|
|
|
SYSTEMTIME st;
|
|
|
|
statstg->type = STGTY_STREAM;
|
|
|
|
GetSystemTime(&st);
|
|
SystemTimeToFileTime((const SYSTEMTIME*)&st, (LPFILETIME)&statstg->mtime);
|
|
statstg->ctime = statstg->atime = statstg->mtime;
|
|
|
|
statstg->cbSize.QuadPart = mTotalLength;
|
|
statstg->grfMode = STGM_READ;
|
|
statstg->grfLocksSupported = LOCK_ONLYONCE;
|
|
statstg->clsid = CLSID_NULL;
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
/*
|
|
* Class nsDataObj
|
|
*/
|
|
|
|
//-----------------------------------------------------
|
|
// construction
|
|
//-----------------------------------------------------
|
|
nsDataObj::nsDataObj(nsIURI* uri)
|
|
: m_cRef(0),
|
|
mTransferable(nullptr),
|
|
mIsAsyncMode(FALSE),
|
|
mIsInOperation(FALSE) {
|
|
mIOThread = new LazyIdleThread(DEFAULT_THREAD_TIMEOUT_MS, "nsDataObj",
|
|
LazyIdleThread::ManualShutdown);
|
|
m_enumFE = new CEnumFormatEtc();
|
|
m_enumFE->AddRef();
|
|
|
|
if (uri) {
|
|
// A URI was obtained, so pass this through to the DataObject
|
|
// so it can create a SourceURL for CF_HTML flavour
|
|
uri->GetSpec(mSourceURL);
|
|
}
|
|
}
|
|
//-----------------------------------------------------
|
|
// destruction
|
|
//-----------------------------------------------------
|
|
nsDataObj::~nsDataObj() {
|
|
mTransferable = nullptr;
|
|
|
|
mDataFlavors.Clear();
|
|
|
|
m_enumFE->Release();
|
|
|
|
// Free arbitrary system formats
|
|
for (uint32_t idx = 0; idx < mDataEntryList.Length(); idx++) {
|
|
CoTaskMemFree(mDataEntryList[idx]->fe.ptd);
|
|
ReleaseStgMedium(&mDataEntryList[idx]->stgm);
|
|
CoTaskMemFree(mDataEntryList[idx]);
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------
|
|
// IUnknown interface methods - see inknown.h for documentation
|
|
//-----------------------------------------------------
|
|
STDMETHODIMP nsDataObj::QueryInterface(REFIID riid, void** ppv) {
|
|
*ppv = nullptr;
|
|
|
|
if ((IID_IUnknown == riid) || (IID_IDataObject == riid)) {
|
|
*ppv = this;
|
|
AddRef();
|
|
return S_OK;
|
|
} else if (IID_IDataObjectAsyncCapability == riid) {
|
|
*ppv = static_cast<IDataObjectAsyncCapability*>(this);
|
|
AddRef();
|
|
return S_OK;
|
|
}
|
|
|
|
return E_NOINTERFACE;
|
|
}
|
|
|
|
//-----------------------------------------------------
|
|
STDMETHODIMP_(ULONG) nsDataObj::AddRef() {
|
|
++m_cRef;
|
|
NS_LOG_ADDREF(this, m_cRef, "nsDataObj", sizeof(*this));
|
|
|
|
// When the first reference is taken, hold our own internal reference.
|
|
if (m_cRef == 1) {
|
|
mKeepAlive = this;
|
|
}
|
|
|
|
return m_cRef;
|
|
}
|
|
|
|
namespace {
|
|
class RemoveTempFileHelper final : public nsIObserver, public nsINamed {
|
|
public:
|
|
explicit RemoveTempFileHelper(nsIFile* aTempFile) : mTempFile(aTempFile) {
|
|
MOZ_ASSERT(mTempFile);
|
|
}
|
|
|
|
// The attach method is seperate from the constructor as we may be addref-ing
|
|
// ourself, and we want to be sure someone has a strong reference to us.
|
|
void Attach() {
|
|
// We need to listen to both the xpcom shutdown message and our timer, and
|
|
// fire when the first of either of these two messages is received.
|
|
nsresult rv;
|
|
rv = NS_NewTimerWithObserver(getter_AddRefs(mTimer), this, 500,
|
|
nsITimer::TYPE_ONE_SHOT);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return;
|
|
}
|
|
|
|
nsCOMPtr<nsIObserverService> observerService =
|
|
do_GetService("@mozilla.org/observer-service;1");
|
|
if (NS_WARN_IF(!observerService)) {
|
|
mTimer->Cancel();
|
|
mTimer = nullptr;
|
|
return;
|
|
}
|
|
observerService->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
|
|
}
|
|
|
|
NS_DECL_ISUPPORTS
|
|
NS_DECL_NSIOBSERVER
|
|
NS_DECL_NSINAMED
|
|
|
|
private:
|
|
~RemoveTempFileHelper() {
|
|
if (mTempFile) {
|
|
mTempFile->Remove(false);
|
|
}
|
|
}
|
|
|
|
nsCOMPtr<nsIFile> mTempFile;
|
|
nsCOMPtr<nsITimer> mTimer;
|
|
};
|
|
|
|
NS_IMPL_ISUPPORTS(RemoveTempFileHelper, nsIObserver, nsINamed);
|
|
|
|
NS_IMETHODIMP
|
|
RemoveTempFileHelper::Observe(nsISupports* aSubject, const char* aTopic,
|
|
const char16_t* aData) {
|
|
// Let's be careful and make sure that we don't die immediately
|
|
RefPtr<RemoveTempFileHelper> grip = this;
|
|
|
|
// Make sure that we aren't called again by destroying references to ourself.
|
|
nsCOMPtr<nsIObserverService> observerService =
|
|
do_GetService("@mozilla.org/observer-service;1");
|
|
if (observerService) {
|
|
observerService->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
|
|
}
|
|
|
|
if (mTimer) {
|
|
mTimer->Cancel();
|
|
mTimer = nullptr;
|
|
}
|
|
|
|
// Remove the tempfile
|
|
if (mTempFile) {
|
|
mTempFile->Remove(false);
|
|
mTempFile = nullptr;
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
RemoveTempFileHelper::GetName(nsACString& aName) {
|
|
aName.AssignLiteral("RemoveTempFileHelper");
|
|
return NS_OK;
|
|
}
|
|
} // namespace
|
|
|
|
//-----------------------------------------------------
|
|
STDMETHODIMP_(ULONG) nsDataObj::Release() {
|
|
--m_cRef;
|
|
|
|
NS_LOG_RELEASE(this, m_cRef, "nsDataObj");
|
|
|
|
// If we hold the last reference, submit release of it to the main thread.
|
|
if (m_cRef == 1 && mKeepAlive) {
|
|
NS_ReleaseOnMainThread("nsDataObj release", mKeepAlive.forget(), true);
|
|
}
|
|
|
|
if (0 != m_cRef) return m_cRef;
|
|
|
|
// We have released our last ref on this object and need to delete the
|
|
// temp file. External app acting as drop target may still need to open the
|
|
// temp file. Addref a timer so it can delay deleting file and destroying
|
|
// this object.
|
|
if (mCachedTempFile) {
|
|
auto helper = MakeRefPtr<RemoveTempFileHelper>(mCachedTempFile);
|
|
mCachedTempFile = nullptr;
|
|
helper->Attach();
|
|
}
|
|
|
|
// In case the destructor ever AddRef/Releases, ensure we don't delete twice
|
|
// or take mKeepAlive as another reference.
|
|
m_cRef = 1;
|
|
|
|
delete this;
|
|
|
|
return 0;
|
|
}
|
|
|
|
//-----------------------------------------------------
|
|
BOOL nsDataObj::FormatsMatch(const FORMATETC& source,
|
|
const FORMATETC& target) const {
|
|
if ((source.cfFormat == target.cfFormat) &&
|
|
(source.dwAspect & target.dwAspect) && (source.tymed & target.tymed)) {
|
|
return TRUE;
|
|
} else {
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------
|
|
// IDataObject methods
|
|
//-----------------------------------------------------
|
|
STDMETHODIMP nsDataObj::GetData(LPFORMATETC aFormat, LPSTGMEDIUM pSTM) {
|
|
if (!mTransferable) return DV_E_FORMATETC;
|
|
|
|
// Hold an extra reference in case we end up spinning the event loop.
|
|
RefPtr<nsDataObj> keepAliveDuringGetData(this);
|
|
|
|
uint32_t dfInx = 0;
|
|
|
|
static CLIPFORMAT fileDescriptorFlavorA =
|
|
::RegisterClipboardFormat(CFSTR_FILEDESCRIPTORA);
|
|
static CLIPFORMAT fileDescriptorFlavorW =
|
|
::RegisterClipboardFormat(CFSTR_FILEDESCRIPTORW);
|
|
static CLIPFORMAT uniformResourceLocatorA =
|
|
::RegisterClipboardFormat(CFSTR_INETURLA);
|
|
static CLIPFORMAT uniformResourceLocatorW =
|
|
::RegisterClipboardFormat(CFSTR_INETURLW);
|
|
static CLIPFORMAT fileFlavor = ::RegisterClipboardFormat(CFSTR_FILECONTENTS);
|
|
static CLIPFORMAT PreferredDropEffect =
|
|
::RegisterClipboardFormat(CFSTR_PREFERREDDROPEFFECT);
|
|
static CLIPFORMAT imagePNGFormat = ::RegisterClipboardFormat(TEXT("PNG"));
|
|
|
|
// Arbitrary system formats are used for image feedback during drag
|
|
// and drop. We are responsible for storing these internally during
|
|
// drag operations.
|
|
LPDATAENTRY pde;
|
|
if (LookupArbitraryFormat(aFormat, &pde, FALSE)) {
|
|
return CopyMediumData(pSTM, &pde->stgm, aFormat, FALSE) ? S_OK
|
|
: E_UNEXPECTED;
|
|
}
|
|
|
|
// Firefox internal formats
|
|
ULONG count;
|
|
FORMATETC fe;
|
|
m_enumFE->Reset();
|
|
while (NOERROR == m_enumFE->Next(1, &fe, &count) &&
|
|
dfInx < mDataFlavors.Length()) {
|
|
nsCString const& df = mDataFlavors.ElementAt(dfInx);
|
|
if (FormatsMatch(fe, *aFormat)) {
|
|
*pSTM = STGMEDIUM{};
|
|
CLIPFORMAT const format = aFormat->cfFormat;
|
|
|
|
// compile-time-constant format indicators:
|
|
switch (format) {
|
|
// Someone is asking for plain or unicode text
|
|
case CF_TEXT:
|
|
case CF_UNICODETEXT:
|
|
return GetText(df, *aFormat, *pSTM);
|
|
|
|
// Some 3rd party apps that receive drag and drop files from the browser
|
|
// window require support for this.
|
|
case CF_HDROP:
|
|
return GetFile(*aFormat, *pSTM);
|
|
|
|
// Someone is asking for an image
|
|
case CF_DIBV5:
|
|
case CF_DIB:
|
|
return GetDib(df, *aFormat, *pSTM, DibType::Bmp);
|
|
|
|
default: /* fallthrough */;
|
|
} // switch
|
|
|
|
// non-compile-time-constant format indicators:
|
|
if (format == imagePNGFormat)
|
|
return GetDib(df, *aFormat, *pSTM, DibType::Png);
|
|
if (format == fileDescriptorFlavorA)
|
|
return GetFileDescriptor(*aFormat, *pSTM, false);
|
|
if (format == fileDescriptorFlavorW)
|
|
return GetFileDescriptor(*aFormat, *pSTM, true);
|
|
if (format == uniformResourceLocatorA)
|
|
return GetUniformResourceLocator(*aFormat, *pSTM, false);
|
|
if (format == uniformResourceLocatorW)
|
|
return GetUniformResourceLocator(*aFormat, *pSTM, true);
|
|
if (format == fileFlavor) return GetFileContents(*aFormat, *pSTM);
|
|
if (format == PreferredDropEffect)
|
|
return GetPreferredDropEffect(*aFormat, *pSTM);
|
|
if (format == nsClipboard::GetWebCustomFormatMapClipboardFormat())
|
|
return GetText(df, *aFormat, *pSTM);
|
|
// MOZ_LOG(gWindowsLog, LogLevel::Info,
|
|
// ("***** nsDataObj::GetData - Unknown format %u\n", format));
|
|
return GetText(df, *aFormat, *pSTM);
|
|
} // if
|
|
dfInx++;
|
|
} // while
|
|
|
|
return DATA_E_FORMATETC;
|
|
}
|
|
|
|
//-----------------------------------------------------
|
|
STDMETHODIMP nsDataObj::GetDataHere(LPFORMATETC pFE, LPSTGMEDIUM pSTM) {
|
|
return E_FAIL;
|
|
}
|
|
|
|
//-----------------------------------------------------
|
|
// Other objects querying to see if we support a
|
|
// particular format
|
|
//-----------------------------------------------------
|
|
STDMETHODIMP nsDataObj::QueryGetData(LPFORMATETC pFE) {
|
|
// Arbitrary system formats are used for image feedback during drag
|
|
// and drop. We are responsible for storing these internally during
|
|
// drag operations.
|
|
LPDATAENTRY pde;
|
|
if (LookupArbitraryFormat(pFE, &pde, FALSE)) return S_OK;
|
|
|
|
// Firefox internal formats
|
|
ULONG count;
|
|
FORMATETC fe;
|
|
m_enumFE->Reset();
|
|
while (NOERROR == m_enumFE->Next(1, &fe, &count)) {
|
|
if (fe.cfFormat == pFE->cfFormat) {
|
|
return S_OK;
|
|
}
|
|
}
|
|
return DV_E_FORMATETC;
|
|
}
|
|
|
|
//-----------------------------------------------------
|
|
STDMETHODIMP nsDataObj::GetCanonicalFormatEtc(LPFORMATETC pFEIn,
|
|
LPFORMATETC pFEOut) {
|
|
return E_NOTIMPL;
|
|
}
|
|
|
|
//-----------------------------------------------------
|
|
STDMETHODIMP nsDataObj::SetData(LPFORMATETC aFormat, LPSTGMEDIUM aMedium,
|
|
BOOL shouldRel) {
|
|
// Arbitrary system formats are used for image feedback during drag
|
|
// and drop. We are responsible for storing these internally during
|
|
// drag operations.
|
|
LPDATAENTRY pde;
|
|
if (LookupArbitraryFormat(aFormat, &pde, TRUE)) {
|
|
// Release the old data the lookup handed us for this format. This
|
|
// may have been set in CopyMediumData when we originally stored the
|
|
// data.
|
|
if (pde->stgm.tymed) {
|
|
ReleaseStgMedium(&pde->stgm);
|
|
memset(&pde->stgm, 0, sizeof(STGMEDIUM));
|
|
}
|
|
|
|
bool result = true;
|
|
if (shouldRel) {
|
|
// If shouldRel is TRUE, the data object called owns the storage medium
|
|
// after the call returns. Store the incoming data in our data array for
|
|
// release when we are destroyed. This is the common case with arbitrary
|
|
// data from explorer.
|
|
pde->stgm = *aMedium;
|
|
} else {
|
|
// Copy the incoming data into our data array. (AFAICT, this never gets
|
|
// called with arbitrary formats for drag images.)
|
|
result = CopyMediumData(&pde->stgm, aMedium, aFormat, TRUE);
|
|
}
|
|
pde->fe.tymed = pde->stgm.tymed;
|
|
|
|
return result ? S_OK : DV_E_TYMED;
|
|
}
|
|
|
|
if (shouldRel) ReleaseStgMedium(aMedium);
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
bool nsDataObj::LookupArbitraryFormat(FORMATETC* aFormat,
|
|
LPDATAENTRY* aDataEntry,
|
|
BOOL aAddorUpdate) {
|
|
*aDataEntry = nullptr;
|
|
|
|
if (aFormat->ptd != nullptr) return false;
|
|
|
|
// See if it's already in our list. If so return the data entry.
|
|
for (uint32_t idx = 0; idx < mDataEntryList.Length(); idx++) {
|
|
if (mDataEntryList[idx]->fe.cfFormat == aFormat->cfFormat &&
|
|
mDataEntryList[idx]->fe.dwAspect == aFormat->dwAspect &&
|
|
mDataEntryList[idx]->fe.lindex == aFormat->lindex) {
|
|
if (aAddorUpdate || (mDataEntryList[idx]->fe.tymed & aFormat->tymed)) {
|
|
// If the caller requests we update, or if the
|
|
// medium type matches, return the entry.
|
|
*aDataEntry = mDataEntryList[idx];
|
|
return true;
|
|
} else {
|
|
// Medium does not match, not found.
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!aAddorUpdate) return false;
|
|
|
|
// Add another entry to mDataEntryList
|
|
LPDATAENTRY dataEntry = (LPDATAENTRY)CoTaskMemAlloc(sizeof(DATAENTRY));
|
|
if (!dataEntry) return false;
|
|
|
|
dataEntry->fe = *aFormat;
|
|
*aDataEntry = dataEntry;
|
|
memset(&dataEntry->stgm, 0, sizeof(STGMEDIUM));
|
|
|
|
// Add this to our IEnumFORMATETC impl. so we can return it when
|
|
// it's requested.
|
|
m_enumFE->AddFormatEtc(aFormat);
|
|
|
|
// Store a copy internally in the arbitrary formats array.
|
|
mDataEntryList.AppendElement(dataEntry);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool nsDataObj::CopyMediumData(STGMEDIUM* aMediumDst, STGMEDIUM* aMediumSrc,
|
|
LPFORMATETC aFormat, BOOL aSetData) {
|
|
STGMEDIUM stgmOut = *aMediumSrc;
|
|
|
|
switch (stgmOut.tymed) {
|
|
case TYMED_ISTREAM:
|
|
stgmOut.pstm->AddRef();
|
|
break;
|
|
case TYMED_ISTORAGE:
|
|
stgmOut.pstg->AddRef();
|
|
break;
|
|
case TYMED_HGLOBAL:
|
|
if (!aMediumSrc->pUnkForRelease) {
|
|
if (aSetData) {
|
|
if (aMediumSrc->tymed != TYMED_HGLOBAL) return false;
|
|
stgmOut.hGlobal =
|
|
OleDuplicateData(aMediumSrc->hGlobal, aFormat->cfFormat, 0);
|
|
if (!stgmOut.hGlobal) return false;
|
|
} else {
|
|
// We are returning this data from LookupArbitraryFormat, indicate to
|
|
// the shell we hold it and will free it.
|
|
stgmOut.pUnkForRelease = static_cast<IDataObject*>(this);
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
|
|
if (stgmOut.pUnkForRelease) stgmOut.pUnkForRelease->AddRef();
|
|
|
|
*aMediumDst = stgmOut;
|
|
|
|
return true;
|
|
}
|
|
|
|
//-----------------------------------------------------
|
|
STDMETHODIMP nsDataObj::EnumFormatEtc(DWORD dwDir, LPENUMFORMATETC* ppEnum) {
|
|
switch (dwDir) {
|
|
case DATADIR_GET:
|
|
m_enumFE->Clone(ppEnum);
|
|
break;
|
|
case DATADIR_SET:
|
|
// fall through
|
|
default:
|
|
*ppEnum = nullptr;
|
|
} // switch
|
|
|
|
if (nullptr == *ppEnum) return E_FAIL;
|
|
|
|
(*ppEnum)->Reset();
|
|
// Clone already AddRefed the result so don't addref it again.
|
|
return NOERROR;
|
|
}
|
|
|
|
//-----------------------------------------------------
|
|
STDMETHODIMP nsDataObj::DAdvise(LPFORMATETC pFE, DWORD dwFlags,
|
|
LPADVISESINK pIAdviseSink, DWORD* pdwConn) {
|
|
return OLE_E_ADVISENOTSUPPORTED;
|
|
}
|
|
|
|
//-----------------------------------------------------
|
|
STDMETHODIMP nsDataObj::DUnadvise(DWORD dwConn) {
|
|
return OLE_E_ADVISENOTSUPPORTED;
|
|
}
|
|
|
|
//-----------------------------------------------------
|
|
STDMETHODIMP nsDataObj::EnumDAdvise(LPENUMSTATDATA* ppEnum) {
|
|
return OLE_E_ADVISENOTSUPPORTED;
|
|
}
|
|
|
|
// IDataObjectAsyncCapability methods
|
|
STDMETHODIMP nsDataObj::EndOperation(HRESULT hResult, IBindCtx* pbcReserved,
|
|
DWORD dwEffects) {
|
|
mIsInOperation = FALSE;
|
|
return S_OK;
|
|
}
|
|
|
|
STDMETHODIMP nsDataObj::GetAsyncMode(BOOL* pfIsOpAsync) {
|
|
*pfIsOpAsync = mIsAsyncMode;
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
STDMETHODIMP nsDataObj::InOperation(BOOL* pfInAsyncOp) {
|
|
*pfInAsyncOp = mIsInOperation;
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
STDMETHODIMP nsDataObj::SetAsyncMode(BOOL fDoOpAsync) {
|
|
mIsAsyncMode = fDoOpAsync;
|
|
return S_OK;
|
|
}
|
|
|
|
STDMETHODIMP nsDataObj::StartOperation(IBindCtx* pbcReserved) {
|
|
mIsInOperation = TRUE;
|
|
return S_OK;
|
|
}
|
|
|
|
//
|
|
// GetDIB
|
|
//
|
|
// Someone is asking for a bitmap. The data in the transferable will be a
|
|
// straight imgIContainer, so just QI it.
|
|
//
|
|
HRESULT
|
|
nsDataObj::GetDib(const nsACString& inFlavor, FORMATETC& aFormat,
|
|
STGMEDIUM& aSTG, DibType aDibType) {
|
|
nsCOMPtr<nsISupports> genericDataWrapper;
|
|
if (NS_FAILED(
|
|
mTransferable->GetTransferData(PromiseFlatCString(inFlavor).get(),
|
|
getter_AddRefs(genericDataWrapper)))) {
|
|
return E_FAIL;
|
|
}
|
|
|
|
nsCOMPtr<imgIContainer> image = do_QueryInterface(genericDataWrapper);
|
|
if (!image) {
|
|
return E_FAIL;
|
|
}
|
|
|
|
nsCOMPtr<imgITools> imgTools =
|
|
do_CreateInstance("@mozilla.org/image/tools;1");
|
|
|
|
nsAutoString options(u""_ns);
|
|
if (aDibType == DibType::Bmp) {
|
|
if (aFormat.cfFormat == CF_DIBV5) {
|
|
options.AssignLiteral("version=5");
|
|
} else {
|
|
options.AssignLiteral("version=3");
|
|
}
|
|
}
|
|
|
|
const nsLiteralCString mimeType = aDibType == DibType::Bmp
|
|
? nsLiteralCString(IMAGE_BMP)
|
|
: nsLiteralCString(IMAGE_PNG);
|
|
nsCOMPtr<nsIInputStream> inputStream;
|
|
nsresult rv = imgTools->EncodeImage(image, mimeType, options,
|
|
getter_AddRefs(inputStream));
|
|
|
|
if (NS_FAILED(rv) || !inputStream) {
|
|
return E_FAIL;
|
|
}
|
|
|
|
nsCOMPtr<imgIEncoder> encoder = do_QueryInterface(inputStream);
|
|
if (!encoder) {
|
|
return E_FAIL;
|
|
}
|
|
|
|
uint32_t size = 0;
|
|
rv = encoder->GetImageBufferUsed(&size);
|
|
if (NS_FAILED(rv) || size <= BFH_LENGTH) {
|
|
return E_FAIL;
|
|
}
|
|
|
|
char* src = nullptr;
|
|
rv = encoder->GetImageBuffer(&src);
|
|
if (NS_FAILED(rv) || !src) {
|
|
return E_FAIL;
|
|
}
|
|
|
|
if (aDibType == DibType::Bmp) {
|
|
// We don't want the BMP file-header for CF_DIB; it only exists in files.
|
|
src += BFH_LENGTH;
|
|
size -= BFH_LENGTH;
|
|
}
|
|
|
|
ScopedOLEMemory<char[]> glob(size);
|
|
if (!glob) {
|
|
return E_FAIL;
|
|
}
|
|
|
|
{
|
|
auto lock = glob.lock();
|
|
::CopyMemory(lock.begin(), src, size);
|
|
}
|
|
|
|
aSTG.tymed = TYMED_HGLOBAL;
|
|
aSTG.hGlobal = glob.forget();
|
|
return S_OK;
|
|
}
|
|
|
|
//
|
|
// GetFileDescriptor
|
|
//
|
|
|
|
HRESULT
|
|
nsDataObj ::GetFileDescriptor(FORMATETC& aFE, STGMEDIUM& aSTG,
|
|
bool aIsUnicode) {
|
|
HRESULT res = S_OK;
|
|
|
|
// How we handle this depends on if we're dealing with an internet
|
|
// shortcut, since those are done under the covers.
|
|
if (IsFlavourPresent(kFilePromiseMime) || IsFlavourPresent(kFileMime)) {
|
|
if (aIsUnicode)
|
|
return GetFileDescriptor_IStreamW(aFE, aSTG);
|
|
else
|
|
return GetFileDescriptor_IStreamA(aFE, aSTG);
|
|
} else if (IsFlavourPresent(kURLMime)) {
|
|
if (aIsUnicode)
|
|
res = GetFileDescriptorInternetShortcutW(aFE, aSTG);
|
|
else
|
|
res = GetFileDescriptorInternetShortcutA(aFE, aSTG);
|
|
} else
|
|
NS_WARNING("Not yet implemented");
|
|
|
|
return res;
|
|
} // GetFileDescriptor
|
|
|
|
//
|
|
HRESULT
|
|
nsDataObj ::GetFileContents(FORMATETC& aFE, STGMEDIUM& aSTG) {
|
|
HRESULT res = S_OK;
|
|
|
|
// How we handle this depends on if we're dealing with an internet
|
|
// shortcut, since those are done under the covers.
|
|
if (IsFlavourPresent(kFilePromiseMime) || IsFlavourPresent(kFileMime))
|
|
return GetFileContents_IStream(aFE, aSTG);
|
|
else if (IsFlavourPresent(kURLMime))
|
|
return GetFileContentsInternetShortcut(aFE, aSTG);
|
|
else
|
|
NS_WARNING("Not yet implemented");
|
|
|
|
return res;
|
|
|
|
} // GetFileContents
|
|
|
|
// Ensure that the supplied name doesn't have invalid characters.
|
|
static void ValidateFilename(nsString& aFilename, bool isShortcut) {
|
|
nsCOMPtr<nsIMIMEService> mimeService = do_GetService("@mozilla.org/mime;1");
|
|
if (NS_WARN_IF(!mimeService)) {
|
|
aFilename.Truncate();
|
|
return;
|
|
}
|
|
|
|
uint32_t flags = nsIMIMEService::VALIDATE_SANITIZE_ONLY;
|
|
if (isShortcut) {
|
|
flags |= nsIMIMEService::VALIDATE_ALLOW_INVALID_FILENAMES;
|
|
}
|
|
|
|
nsAutoString outFilename;
|
|
mimeService->ValidateFileNameForSaving(aFilename, EmptyCString(), flags,
|
|
outFilename);
|
|
aFilename = outFilename;
|
|
}
|
|
|
|
//
|
|
// Given a unicode string, convert it to a valid local charset filename
|
|
// and append the .url extension to be used for a shortcut file.
|
|
// This ensures that we do not cut MBCS characters in the middle.
|
|
//
|
|
// It would seem that this is more functionality suited to being in nsIFile.
|
|
//
|
|
static bool CreateURLFilenameFromTextA(nsAutoString& aText, char* aFilename) {
|
|
if (aText.IsEmpty()) {
|
|
return false;
|
|
}
|
|
aText.AppendLiteral(".url");
|
|
ValidateFilename(aText, true);
|
|
if (aText.IsEmpty()) {
|
|
return false;
|
|
}
|
|
|
|
// ValidateFilename should already be checking the filename length, but do
|
|
// an extra check to verify for the local code page that the converted text
|
|
// doesn't go over MAX_PATH and just return false if it does.
|
|
char defaultChar = '_';
|
|
int currLen = WideCharToMultiByte(
|
|
CP_ACP, WC_COMPOSITECHECK | WC_DEFAULTCHAR | WC_NO_BEST_FIT_CHARS,
|
|
aText.get(), -1, aFilename, MAX_PATH, &defaultChar, nullptr);
|
|
return currLen != 0;
|
|
}
|
|
|
|
// Wide character version of CreateURLFilenameFromTextA
|
|
static bool CreateURLFilenameFromTextW(nsAutoString& aText,
|
|
wchar_t* aFilename) {
|
|
if (aText.IsEmpty()) {
|
|
return false;
|
|
}
|
|
aText.AppendLiteral(".url");
|
|
ValidateFilename(aText, true);
|
|
if (aText.IsEmpty() || aText.Length() >= MAX_PATH) {
|
|
return false;
|
|
}
|
|
|
|
wcscpy(&aFilename[0], aText.get());
|
|
return true;
|
|
}
|
|
|
|
#define PAGEINFO_PROPERTIES "chrome://navigator/locale/pageInfo.properties"
|
|
|
|
static bool GetLocalizedString(const char* aName, nsAString& aString) {
|
|
nsCOMPtr<nsIStringBundleService> stringService =
|
|
mozilla::components::StringBundle::Service();
|
|
if (!stringService) return false;
|
|
|
|
nsCOMPtr<nsIStringBundle> stringBundle;
|
|
nsresult rv = stringService->CreateBundle(PAGEINFO_PROPERTIES,
|
|
getter_AddRefs(stringBundle));
|
|
if (NS_FAILED(rv)) return false;
|
|
|
|
rv = stringBundle->GetStringFromName(aName, aString);
|
|
return NS_SUCCEEDED(rv);
|
|
}
|
|
|
|
static bool HasWebScheme(nsIURI* aUri) {
|
|
nsAutoCString scheme;
|
|
NS_ENSURE_SUCCESS(aUri->GetScheme(scheme), false);
|
|
return scheme.EqualsLiteral("http") || scheme.EqualsLiteral("https");
|
|
}
|
|
|
|
bool nsDataObj::ShortcutUrlHasWebScheme() {
|
|
nsAutoString urlString;
|
|
NS_ENSURE_SUCCESS(ExtractShortcutURL(urlString), false);
|
|
nsCOMPtr<nsIURI> uri;
|
|
NS_ENSURE_SUCCESS(NS_NewURI(getter_AddRefs(uri), urlString), false);
|
|
return HasWebScheme(uri);
|
|
}
|
|
|
|
//
|
|
// GetFileDescriptorInternetShortcut
|
|
//
|
|
// Create the special format for an internet shortcut and build up the data
|
|
// structures the shell is expecting.
|
|
//
|
|
HRESULT
|
|
nsDataObj ::GetFileDescriptorInternetShortcutA(FORMATETC& aFE,
|
|
STGMEDIUM& aSTG) {
|
|
// get the title of the shortcut
|
|
nsAutoString title;
|
|
if (NS_FAILED(ExtractShortcutTitle(title))) return E_OUTOFMEMORY;
|
|
|
|
// For non-http schemes, allocate space for two FILEDESCRIPTOR entries:
|
|
// the .url file plus a ":Zone.Identifier" ADS so the dropped shortcut is
|
|
// marked Internet-zone (untrusted).
|
|
bool isWebScheme = ShortcutUrlHasWebScheme();
|
|
size_t allocSize = sizeof(FILEGROUPDESCRIPTORA);
|
|
if (!isWebScheme) {
|
|
allocSize += sizeof(FILEDESCRIPTORA);
|
|
}
|
|
HGLOBAL fileGroupDescHandle =
|
|
::GlobalAlloc(GMEM_ZEROINIT | GMEM_SHARE, allocSize);
|
|
if (!fileGroupDescHandle) return E_OUTOFMEMORY;
|
|
|
|
LPFILEGROUPDESCRIPTORA fileGroupDescA =
|
|
reinterpret_cast<LPFILEGROUPDESCRIPTORA>(
|
|
::GlobalLock(fileGroupDescHandle));
|
|
if (!fileGroupDescA) {
|
|
::GlobalFree(fileGroupDescHandle);
|
|
return E_OUTOFMEMORY;
|
|
}
|
|
|
|
// get a valid filename in the following order: 1) from the page title,
|
|
// 2) localized string for an untitled page, 3) just use "Untitled.url"
|
|
if (!CreateURLFilenameFromTextA(title, fileGroupDescA->fgd[0].cFileName)) {
|
|
nsAutoString untitled;
|
|
if (!GetLocalizedString("noPageTitle", untitled) ||
|
|
!CreateURLFilenameFromTextA(untitled,
|
|
fileGroupDescA->fgd[0].cFileName)) {
|
|
strcpy(fileGroupDescA->fgd[0].cFileName, "Untitled.url");
|
|
}
|
|
}
|
|
fileGroupDescA->fgd[0].dwFlags = FD_LINKUI;
|
|
|
|
size_t const mainLen = strnlen(fileGroupDescA->fgd[0].cFileName, MAX_PATH);
|
|
fileGroupDescA->cItems = 1;
|
|
if (!isWebScheme) {
|
|
// Build the ":Zone.Identifier" ADS entry.
|
|
// If appending the suffix would overflow, refuse the entire descriptor.
|
|
constexpr char kAdsSuffix[] = ":Zone.Identifier";
|
|
constexpr size_t kAdsSuffixSize =
|
|
sizeof(kAdsSuffix); // includes terminator
|
|
if (mainLen + kAdsSuffixSize > MAX_PATH) {
|
|
::GlobalUnlock(fileGroupDescHandle);
|
|
::GlobalFree(fileGroupDescHandle);
|
|
return HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND);
|
|
}
|
|
memcpy(fileGroupDescA->fgd[1].cFileName, fileGroupDescA->fgd[0].cFileName,
|
|
mainLen);
|
|
memcpy(fileGroupDescA->fgd[1].cFileName + mainLen, kAdsSuffix,
|
|
kAdsSuffixSize);
|
|
fileGroupDescA->cItems++;
|
|
}
|
|
|
|
::GlobalUnlock(fileGroupDescHandle);
|
|
aSTG.hGlobal = fileGroupDescHandle;
|
|
aSTG.tymed = TYMED_HGLOBAL;
|
|
|
|
return S_OK;
|
|
} // GetFileDescriptorInternetShortcutA
|
|
|
|
HRESULT
|
|
nsDataObj ::GetFileDescriptorInternetShortcutW(FORMATETC& aFE,
|
|
STGMEDIUM& aSTG) {
|
|
// get the title of the shortcut
|
|
nsAutoString title;
|
|
if (NS_FAILED(ExtractShortcutTitle(title))) return E_OUTOFMEMORY;
|
|
|
|
// For non-http schemes, allocate space for two FILEDESCRIPTOR entries:
|
|
// the .url file plus a ":Zone.Identifier" ADS so the dropped shortcut is
|
|
// marked Internet-zone (untrusted).
|
|
bool isWebScheme = ShortcutUrlHasWebScheme();
|
|
size_t allocSize = sizeof(FILEGROUPDESCRIPTORW);
|
|
if (!isWebScheme) {
|
|
allocSize += sizeof(FILEDESCRIPTORW);
|
|
}
|
|
HGLOBAL fileGroupDescHandle =
|
|
::GlobalAlloc(GMEM_ZEROINIT | GMEM_SHARE, allocSize);
|
|
if (!fileGroupDescHandle) return E_OUTOFMEMORY;
|
|
|
|
LPFILEGROUPDESCRIPTORW fileGroupDescW =
|
|
reinterpret_cast<LPFILEGROUPDESCRIPTORW>(
|
|
::GlobalLock(fileGroupDescHandle));
|
|
if (!fileGroupDescW) {
|
|
::GlobalFree(fileGroupDescHandle);
|
|
return E_OUTOFMEMORY;
|
|
}
|
|
|
|
// get a valid filename in the following order: 1) from the page title,
|
|
// 2) localized string for an untitled page, 3) just use "Untitled.url"
|
|
if (!CreateURLFilenameFromTextW(title, fileGroupDescW->fgd[0].cFileName)) {
|
|
nsAutoString untitled;
|
|
if (!GetLocalizedString("noPageTitle", untitled) ||
|
|
!CreateURLFilenameFromTextW(untitled,
|
|
fileGroupDescW->fgd[0].cFileName)) {
|
|
wcscpy(fileGroupDescW->fgd[0].cFileName, L"Untitled.url");
|
|
}
|
|
}
|
|
fileGroupDescW->fgd[0].dwFlags = FD_LINKUI;
|
|
|
|
size_t const mainLen = wcsnlen(fileGroupDescW->fgd[0].cFileName, MAX_PATH);
|
|
fileGroupDescW->cItems = 1;
|
|
if (!isWebScheme) {
|
|
// Build the ":Zone.Identifier" ADS entry.
|
|
// If appending the suffix would overflow, refuse the entire descriptor.
|
|
constexpr WCHAR kAdsSuffix[] = L":Zone.Identifier";
|
|
constexpr size_t kAdsSuffixLen =
|
|
(sizeof(kAdsSuffix) / sizeof(WCHAR)); // includes terminator
|
|
if (mainLen + kAdsSuffixLen > MAX_PATH) {
|
|
::GlobalUnlock(fileGroupDescHandle);
|
|
::GlobalFree(fileGroupDescHandle);
|
|
return HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND);
|
|
}
|
|
wmemcpy(fileGroupDescW->fgd[1].cFileName, fileGroupDescW->fgd[0].cFileName,
|
|
mainLen);
|
|
wmemcpy(fileGroupDescW->fgd[1].cFileName + mainLen, kAdsSuffix,
|
|
kAdsSuffixLen);
|
|
fileGroupDescW->cItems++;
|
|
}
|
|
|
|
::GlobalUnlock(fileGroupDescHandle);
|
|
aSTG.hGlobal = fileGroupDescHandle;
|
|
aSTG.tymed = TYMED_HGLOBAL;
|
|
|
|
return S_OK;
|
|
} // GetFileDescriptorInternetShortcutW
|
|
|
|
//
|
|
// GetFileContentsInternetShortcut
|
|
//
|
|
// Create the special format for an internet shortcut and build up the data
|
|
// structures the shell is expecting.
|
|
//
|
|
HRESULT
|
|
nsDataObj ::GetFileContentsInternetShortcut(FORMATETC& aFE, STGMEDIUM& aSTG) {
|
|
// Treat aFE.lindex = [-1,1] as requests for the URL file. Anything else is
|
|
// invalid.
|
|
if (aFE.lindex < -1 || aFE.lindex > 1) {
|
|
return DV_E_LINDEX;
|
|
}
|
|
|
|
nsAutoString urlString;
|
|
if (NS_FAILED(ExtractShortcutURL(urlString))) return E_OUTOFMEMORY;
|
|
|
|
nsCOMPtr<nsIURI> uri;
|
|
nsresult rv = NS_NewURI(getter_AddRefs(uri), urlString);
|
|
if (NS_FAILED(rv)) {
|
|
return E_FAIL;
|
|
}
|
|
|
|
// The descriptor advertises two entries: the .url content (lindex 0) and
|
|
// the ":Zone.Identifier" ADS that marks it as Internet-zone (lindex 1).
|
|
if (aFE.lindex == 1) {
|
|
if (HasWebScheme(uri)) {
|
|
// Zone index is not valid for web urls.
|
|
return DV_E_LINDEX;
|
|
}
|
|
constexpr char kZoneIdContent[] = "[ZoneTransfer]\r\nZoneId=3\r\n";
|
|
constexpr size_t kZoneIdLen = sizeof(kZoneIdContent) - 1;
|
|
|
|
nsAutoGlobalMem globalMem(nsHGLOBAL(::GlobalAlloc(GMEM_SHARE, kZoneIdLen)));
|
|
if (!globalMem) {
|
|
return E_OUTOFMEMORY;
|
|
}
|
|
char* contents = reinterpret_cast<char*>(::GlobalLock(globalMem.get()));
|
|
if (!contents) {
|
|
return E_OUTOFMEMORY;
|
|
}
|
|
memcpy(contents, kZoneIdContent, kZoneIdLen);
|
|
::GlobalUnlock(globalMem.get());
|
|
|
|
if (aFE.tymed & TYMED_ISTREAM) {
|
|
RefPtr<IStream> stream = new CMemStream(
|
|
globalMem.disown(), kZoneIdLen, already_AddRefed<AutoCloseEvent>());
|
|
stream.forget(&aSTG.pstm);
|
|
aSTG.tymed = TYMED_ISTREAM;
|
|
} else {
|
|
aSTG.hGlobal = globalMem.disown();
|
|
aSTG.tymed = TYMED_HGLOBAL;
|
|
}
|
|
return S_OK;
|
|
}
|
|
|
|
static const char* kShellIconPref = "browser.shell.shortcutFavicons";
|
|
|
|
nsAutoCString asciiUrl;
|
|
rv = uri->GetAsciiSpec(asciiUrl);
|
|
if (NS_FAILED(rv)) {
|
|
return E_FAIL;
|
|
}
|
|
|
|
RefPtr<AutoCloseEvent> event;
|
|
|
|
const char* shortcutFormatStr;
|
|
int totalLen;
|
|
nsCString asciiPath;
|
|
if (!Preferences::GetBool(kShellIconPref, true)) {
|
|
shortcutFormatStr = "[InternetShortcut]\r\nURL=%s\r\n";
|
|
const int formatLen = strlen(shortcutFormatStr) - 2; // don't include %s
|
|
totalLen = formatLen + asciiUrl.Length(); // don't include null character
|
|
} else {
|
|
nsCOMPtr<nsIFile> icoFile;
|
|
|
|
nsAutoString uriHash;
|
|
|
|
event = new AutoCloseEvent();
|
|
if (!event->IsInited()) {
|
|
return E_FAIL;
|
|
}
|
|
|
|
auto e = MakeRefPtr<AutoSetEvent>(WrapNotNull(event));
|
|
mozilla::widget::FaviconHelper::ObtainCachedIconFile(
|
|
uri, uriHash, mIOThread, true,
|
|
NS_NewRunnableFunction(
|
|
"FaviconHelper::RefreshDesktop", [e = std::move(e)] {
|
|
if (e->IsWaiting()) {
|
|
// Unblock IStream:::Read.
|
|
e->Signal();
|
|
} else {
|
|
// We could not wait until the favicon was available. We have
|
|
// to refresh to refect the favicon.
|
|
SendNotifyMessage(HWND_BROADCAST, WM_SETTINGCHANGE,
|
|
SPI_SETNONCLIENTMETRICS, 0);
|
|
}
|
|
}));
|
|
|
|
rv = mozilla::widget::FaviconHelper::GetOutputIconPath(uri, icoFile, true);
|
|
NS_ENSURE_SUCCESS(rv, E_FAIL);
|
|
nsString path;
|
|
rv = icoFile->GetPath(path);
|
|
NS_ENSURE_SUCCESS(rv, E_FAIL);
|
|
|
|
if (IsAsciiNullTerminated(static_cast<const char16_t*>(path.get()))) {
|
|
LossyCopyUTF16toASCII(path, asciiPath);
|
|
shortcutFormatStr =
|
|
"[InternetShortcut]\r\nURL=%s\r\n"
|
|
"IDList=\r\nHotKey=0\r\nIconFile=%s\r\n"
|
|
"IconIndex=0\r\n";
|
|
} else {
|
|
int len = WideCharToMultiByte(CP_UTF7, 0, path.getW(), path.Length(),
|
|
nullptr, 0, nullptr, nullptr);
|
|
NS_ENSURE_TRUE(len > 0, E_FAIL);
|
|
asciiPath.SetLength(len);
|
|
WideCharToMultiByte(CP_UTF7, 0, path.getW(), path.Length(),
|
|
asciiPath.BeginWriting(), len, nullptr, nullptr);
|
|
shortcutFormatStr =
|
|
"[InternetShortcut]\r\nURL=%s\r\n"
|
|
"IDList=\r\nHotKey=0\r\nIconIndex=0\r\n"
|
|
"[InternetShortcut.W]\r\nIconFile=%s\r\n";
|
|
}
|
|
const int formatLen = strlen(shortcutFormatStr) - 2 * 2; // no %s twice
|
|
totalLen = formatLen + asciiUrl.Length() +
|
|
asciiPath.Length(); // we don't want a null character on the end
|
|
}
|
|
|
|
// create a global memory area and build up the file contents w/in it
|
|
nsAutoGlobalMem globalMem(nsHGLOBAL(::GlobalAlloc(GMEM_SHARE, totalLen)));
|
|
if (!globalMem) return E_OUTOFMEMORY;
|
|
|
|
char* contents = reinterpret_cast<char*>(::GlobalLock(globalMem.get()));
|
|
if (!contents) {
|
|
return E_OUTOFMEMORY;
|
|
}
|
|
|
|
// NOTE: we intentionally use the Microsoft version of snprintf here because
|
|
// it does NOT null
|
|
// terminate strings which reach the maximum size of the buffer. Since we know
|
|
// that the formatted length here is totalLen, this call to _snprintf will
|
|
// format the string into the buffer without appending the null character.
|
|
|
|
if (!Preferences::GetBool(kShellIconPref, true)) {
|
|
_snprintf(contents, totalLen, shortcutFormatStr, asciiUrl.get());
|
|
} else {
|
|
_snprintf(contents, totalLen, shortcutFormatStr, asciiUrl.get(),
|
|
asciiPath.get());
|
|
}
|
|
|
|
::GlobalUnlock(globalMem.get());
|
|
|
|
if (aFE.tymed & TYMED_ISTREAM) {
|
|
if (!mIsInOperation) {
|
|
// The drop target didn't initiate an async operation.
|
|
// We can't block CMemStream::Read.
|
|
event = nullptr;
|
|
}
|
|
auto stream =
|
|
MakeRefPtr<CMemStream>(globalMem.disown(), totalLen, event.forget());
|
|
stream.forget(&aSTG.pstm);
|
|
aSTG.tymed = TYMED_ISTREAM;
|
|
} else {
|
|
if (event && event->IsInited()) {
|
|
event->Signal(); // We can't block reading the global memory
|
|
}
|
|
aSTG.hGlobal = globalMem.disown();
|
|
aSTG.tymed = TYMED_HGLOBAL;
|
|
}
|
|
|
|
return S_OK;
|
|
} // GetFileContentsInternetShortcut
|
|
|
|
// check if specified flavour is present in the transferable
|
|
bool nsDataObj ::IsFlavourPresent(const char* inFlavour) {
|
|
bool retval = false;
|
|
NS_ENSURE_TRUE(mTransferable, false);
|
|
|
|
// get the list of flavors available in the transferable
|
|
nsTArray<nsCString> flavors;
|
|
nsresult rv = mTransferable->FlavorsTransferableCanExport(flavors);
|
|
NS_ENSURE_SUCCESS(rv, false);
|
|
|
|
// try to find requested flavour
|
|
for (uint32_t i = 0; i < flavors.Length(); ++i) {
|
|
if (flavors[i].Equals(inFlavour)) {
|
|
retval = true; // found it!
|
|
break;
|
|
}
|
|
} // for each flavor
|
|
|
|
return retval;
|
|
}
|
|
|
|
HRESULT nsDataObj::GetPreferredDropEffect(FORMATETC& aFE, STGMEDIUM& aSTG) {
|
|
HRESULT res = S_OK;
|
|
aSTG.tymed = TYMED_HGLOBAL;
|
|
aSTG.pUnkForRelease = nullptr;
|
|
|
|
ScopedOLEMemory<DWORD> hGlobalMemory;
|
|
if (hGlobalMemory) {
|
|
// The PreferredDropEffect clipboard format is only registered if a
|
|
// drag/drop of an image happens from Mozilla to the desktop. We want its
|
|
// value to be DROPEFFECT_MOVE in that case so that the file is moved from
|
|
// the temporary location, not copied. This value should, ideally, be set on
|
|
// the data object via SetData() but our IDataObject implementation doesn't
|
|
// implement SetData. It adds data to the data object lazily only when the
|
|
// drop target asks for it.
|
|
*hGlobalMemory.lock() = (DWORD)DROPEFFECT_MOVE;
|
|
} else {
|
|
res = E_OUTOFMEMORY;
|
|
}
|
|
aSTG.hGlobal = hGlobalMemory.forget();
|
|
return res;
|
|
}
|
|
|
|
//-----------------------------------------------------
|
|
HRESULT nsDataObj::GetText(const nsACString& aDataFlavor, FORMATETC& aFE,
|
|
STGMEDIUM& aSTG) {
|
|
// assignDataToStg
|
|
//
|
|
// Helper function to fill the STG with a block of data.
|
|
auto const assignDataToStg = [&aSTG](void* data, size_t extent) -> HRESULT {
|
|
aSTG.tymed = TYMED_HGLOBAL;
|
|
aSTG.pUnkForRelease = nullptr;
|
|
|
|
ScopedOLEMemory<char[]> hGlobalMemory(extent);
|
|
if (hGlobalMemory) {
|
|
auto dest = hGlobalMemory.lock();
|
|
memcpy(dest.get(), data, extent);
|
|
}
|
|
|
|
aSTG.hGlobal = hGlobalMemory.forget();
|
|
|
|
return S_OK;
|
|
};
|
|
|
|
// kWebCustomFormatMapType is synthetic: the JSON is held on the data object
|
|
// (set by nsClipboard::SetupNativeDataObject) rather than in the
|
|
// transferable. Serve it without the trailing UTF-16 null pad.
|
|
if (aDataFlavor.EqualsLiteral(kWebCustomFormatMapType)) {
|
|
MOZ_ASSERT(!mWebCustomFormatMapJson.IsEmpty());
|
|
return assignDataToStg(const_cast<char*>(mWebCustomFormatMapJson.get()),
|
|
mWebCustomFormatMapJson.Length());
|
|
}
|
|
|
|
const nsPromiseFlatCString& flavorStr = PromiseFlatCString(aDataFlavor);
|
|
|
|
nsCOMPtr<nsISupports> genericDataWrapper;
|
|
nsresult rv = mTransferable->GetTransferData(
|
|
flavorStr.get(), getter_AddRefs(genericDataWrapper));
|
|
if (NS_FAILED(rv) || !genericDataWrapper) {
|
|
return E_FAIL;
|
|
}
|
|
|
|
// data is a possibly-wide NUL-terminated string. len is its strlen() -- not
|
|
// its allocation length!
|
|
auto const [data, len] = [&]() {
|
|
void* data = nullptr;
|
|
uint32_t len;
|
|
nsPrimitiveHelpers::CreateDataFromPrimitive(
|
|
nsDependentCString(flavorStr.get()), genericDataWrapper, &data, &len);
|
|
return std::tuple{data, size_t(len)};
|
|
}();
|
|
if (!data) return E_FAIL;
|
|
|
|
// CreateDataFromPrimitive allocates memory; free it on exit.
|
|
auto const _release_data =
|
|
mozilla::MakeScopeExit([data = data]() { ::free(data); });
|
|
|
|
// We play games under the hood and advertise flavors that we know we can
|
|
// support, only they require a bit of conversion or munging of the data. Do
|
|
// that here.
|
|
//
|
|
// The transferable gives us data that is null-terminated, but this isn't
|
|
// reflected in the |len| parameter. Windows apps expect this null to be there
|
|
// so bump our data buffer by the appropriate size to account for the null
|
|
// (one char for CF_TEXT, one char16_t for CF_UNICODETEXT).
|
|
|
|
if (aFE.cfFormat == CF_TEXT) {
|
|
// Someone is asking for text/plain; convert the unicode (assuming it's
|
|
// present) to text with the correct platform encoding.
|
|
//
|
|
// One UTF-16 code unit can encode to more than two bytes: U+0800..U+FFFF
|
|
// takes three when the ANSI code page is UTF-8. Size the buffer from the
|
|
// code page's own maximum rather than assuming two.
|
|
// The code page is fixed until a Windows reboot.
|
|
static UINT sMaxCharSize = []() {
|
|
CPINFO cpInfo;
|
|
if (!::GetCPInfo(CP_ACP, &cpInfo)) {
|
|
MOZ_ASSERT_UNREACHABLE("Couldn't get code page info?");
|
|
// The max ANSI code page character size at the moment is four bytes.
|
|
return 4u;
|
|
}
|
|
return cpInfo.MaxCharSize;
|
|
}();
|
|
|
|
// |len| is a byte count; the conversion counts UTF-16 code units, and
|
|
// includes the terminating null.
|
|
CheckedInt<int> const unitCount = CheckedInt<int>(len) / 2 + 1;
|
|
CheckedInt<int> const bufferSize = unitCount * sMaxCharSize;
|
|
if (!bufferSize.isValid()) {
|
|
return E_FAIL;
|
|
}
|
|
|
|
char* plainTextData = static_cast<char*>(moz_xmalloc(bufferSize.value()));
|
|
auto const _release =
|
|
mozilla::MakeScopeExit([plainTextData]() { ::free(plainTextData); });
|
|
|
|
char16_t* castedUnicode = reinterpret_cast<char16_t*>(data);
|
|
int32_t plainTextLen = WideCharToMultiByte(
|
|
CP_ACP, 0, (LPCWSTR)castedUnicode, unitCount.value(), plainTextData,
|
|
bufferSize.value(), NULL, NULL);
|
|
|
|
if (plainTextLen) {
|
|
return assignDataToStg(plainTextData, plainTextLen);
|
|
}
|
|
|
|
NS_WARNING("Oh no, couldn't convert unicode to plain text");
|
|
return E_FAIL;
|
|
}
|
|
|
|
if (aFE.cfFormat == nsClipboard::GetHtmlClipboardFormat()) {
|
|
// Someone is asking for win32's HTML flavor. Convert our html fragment
|
|
// from unicode to UTF-8 then put it into a format specified by msft.
|
|
NS_ConvertUTF16toUTF8 converter(reinterpret_cast<char16_t*>(data));
|
|
char* utf8HTML = nullptr;
|
|
nsresult rv =
|
|
BuildPlatformHTML(converter.get(), &utf8HTML); // null terminates
|
|
auto const _release =
|
|
mozilla::MakeScopeExit([utf8HTML]() { ::free(utf8HTML); });
|
|
|
|
if (NS_SUCCEEDED(rv) && utf8HTML) {
|
|
// return our HTML data. Don't forget the null.
|
|
return assignDataToStg(utf8HTML, strlen(utf8HTML) + sizeof(char));
|
|
}
|
|
|
|
NS_WARNING("Oh no, couldn't convert to HTML");
|
|
return E_FAIL;
|
|
}
|
|
|
|
// We assume that any data-format that isn't caught above can be satisfied by
|
|
// Unicode text. (This may be an erroneous assumption, but seems to have been
|
|
// true so far.) Web custom formats are byte payloads (nsISupportsCString), so
|
|
// they must skip the trailing UTF-16 null pad just like the legacy custom
|
|
// clipboard format does.
|
|
bool const excludeNull =
|
|
aFE.cfFormat == nsClipboard::GetCustomClipboardFormat() ||
|
|
StringBeginsWith(aDataFlavor, nsLiteralCString(kWebCustomFormatPrefix));
|
|
|
|
return assignDataToStg(data, len + (excludeNull ? 0 : sizeof(char16_t)));
|
|
}
|
|
|
|
//-----------------------------------------------------
|
|
HRESULT nsDataObj::GetFile(FORMATETC& aFE, STGMEDIUM& aSTG) {
|
|
ULONG count;
|
|
FORMATETC fe;
|
|
|
|
// We want to prefer kFileMime over kFilePromiseMime, and only
|
|
// fall back to kNativeImageMime if neither of those are present, since
|
|
// kNativeImageMime isn't really a file and we'll have to convert it to
|
|
// either PNG or BMP regardless of the original format.
|
|
enum class FlavorType : uint8_t {
|
|
eNone = 0,
|
|
eNativeImageMime = 1,
|
|
eFilePromiseMime = 2,
|
|
eFileMime = 3,
|
|
};
|
|
FlavorType flavorType = FlavorType::eNone;
|
|
m_enumFE->Reset();
|
|
for (const auto& dataFlavor : mDataFlavors) {
|
|
if (m_enumFE->Next(1, &fe, &count) != NOERROR) {
|
|
break;
|
|
}
|
|
|
|
if (dataFlavor.EqualsLiteral(kFileMime)) {
|
|
flavorType = std::max(FlavorType::eFileMime, flavorType);
|
|
} else if (dataFlavor.EqualsLiteral(kFilePromiseMime)) {
|
|
flavorType = std::max(FlavorType::eFilePromiseMime, flavorType);
|
|
} else if (dataFlavor.EqualsLiteral(kNativeImageMime)) {
|
|
flavorType = std::max(FlavorType::eNativeImageMime, flavorType);
|
|
}
|
|
}
|
|
|
|
switch (flavorType) {
|
|
case FlavorType::eFileMime:
|
|
return DropFile(aFE, aSTG);
|
|
case FlavorType::eFilePromiseMime:
|
|
return DropTempFile(aFE, aSTG);
|
|
case FlavorType::eNativeImageMime:
|
|
return DropImage(aFE, aSTG);
|
|
case FlavorType::eNone:
|
|
return E_FAIL;
|
|
}
|
|
MOZ_ASSERT_UNREACHABLE("Unexpected flavor type");
|
|
return E_FAIL;
|
|
}
|
|
|
|
static HRESULT AssignDropfile(STGMEDIUM& aSTG, nsAString const& aPath) {
|
|
// Struct describing the data in the OLE memory space. Marginally cleaner than
|
|
// completely-manual pointer manipulation.
|
|
struct DFWithPaths {
|
|
DROPFILES dropfiles;
|
|
// An epsilon-terminated list of NUL-terminated strings.
|
|
// See the CF_HDROP shell clipboard format for more info.
|
|
WCHAR paths[];
|
|
};
|
|
|
|
// C++ doesn't have a dependent-type system robust enough for ScopedOLEMemory
|
|
// to handle a struct-with-flexible-array-member well, so we eschew it here
|
|
// for the less-strongly-typed (and slightly more awkward) nsAutoGlobalMem.
|
|
size_t const allocSize =
|
|
// Size of the initial header block...
|
|
sizeof(DFWithPaths) +
|
|
// ... size of the first path...
|
|
((aPath.Length() + 1) * sizeof(WCHAR)) +
|
|
// ... and size of the terminating empty string.
|
|
sizeof(L"");
|
|
|
|
nsAutoGlobalMem hGlobalMemory(
|
|
nsHGLOBAL(::GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, allocSize)));
|
|
|
|
if (!hGlobalMemory) {
|
|
return E_FAIL;
|
|
}
|
|
|
|
{
|
|
ScopedOLELock<DFWithPaths*> pDFWithPaths(hGlobalMemory.get());
|
|
|
|
// First, populate the dropfile structure...
|
|
DROPFILES* pDropFile = &pDFWithPaths->dropfiles;
|
|
pDropFile->pFiles =
|
|
offsetof(DFWithPaths, paths) - offsetof(DFWithPaths, dropfiles);
|
|
pDropFile->fNC = 0;
|
|
pDropFile->pt.x = 0;
|
|
pDropFile->pt.y = 0;
|
|
pDropFile->fWide = TRUE;
|
|
|
|
// ... then copy the filename into `paths`.
|
|
WCHAR* dest = pDFWithPaths->paths;
|
|
WCHAR* after_dest [[maybe_unused]] =
|
|
std::copy_n(aPath.BeginReading(), aPath.Length(), dest);
|
|
|
|
// Two NULs are needed after the file name; the GMEM_ZEROINIT above should
|
|
// provide them.
|
|
size_t const offset [[maybe_unused]] =
|
|
(char*)after_dest - (char*)pDFWithPaths.get();
|
|
MOZ_ASSERT(allocSize - offset == sizeof(WCHAR) * 2);
|
|
MOZ_ASSERT(after_dest[0] == L'\0');
|
|
MOZ_ASSERT(after_dest[1] == L'\0');
|
|
}
|
|
|
|
aSTG = {
|
|
.tymed = TYMED_HGLOBAL,
|
|
.hGlobal = hGlobalMemory.disown(),
|
|
.pUnkForRelease = nullptr,
|
|
};
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
HRESULT nsDataObj::DropFile(FORMATETC& aFE, STGMEDIUM& aSTG) {
|
|
nsresult rv;
|
|
nsCOMPtr<nsISupports> genericDataWrapper;
|
|
|
|
if (NS_FAILED(mTransferable->GetTransferData(
|
|
kFileMime, getter_AddRefs(genericDataWrapper)))) {
|
|
return E_FAIL;
|
|
}
|
|
nsCOMPtr<nsIFile> file(do_QueryInterface(genericDataWrapper));
|
|
if (!file) return E_FAIL;
|
|
|
|
nsAutoString path;
|
|
rv = file->GetPath(path);
|
|
if (NS_FAILED(rv)) return E_FAIL;
|
|
|
|
return AssignDropfile(aSTG, path);
|
|
}
|
|
|
|
HRESULT nsDataObj::DropImage(FORMATETC& /* aFE */, STGMEDIUM& aSTG) {
|
|
nsresult rv;
|
|
if (!mCachedTempFile) {
|
|
nsCOMPtr<nsISupports> genericDataWrapper;
|
|
|
|
if (NS_FAILED(mTransferable->GetTransferData(
|
|
kNativeImageMime, getter_AddRefs(genericDataWrapper)))) {
|
|
return E_FAIL;
|
|
}
|
|
nsCOMPtr<imgIContainer> image(do_QueryInterface(genericDataWrapper));
|
|
if (!image) return E_FAIL;
|
|
|
|
nsCOMPtr<imgITools> imgTools =
|
|
do_CreateInstance("@mozilla.org/image/tools;1");
|
|
nsCOMPtr<nsIInputStream> inputStream;
|
|
|
|
// Select the image encoding.
|
|
//
|
|
// If we get here, the negotiation phase selected "CF_HDROP"... which
|
|
// unfortunately means "file", rather than something more useful like "file
|
|
// of type XYZ". As of 2024, though, it seems that pretty much everything in
|
|
// the ecosystem understands PNG, so we just default to that (with a config
|
|
// pref to enable fallback to BMP for older recipients).
|
|
nsCString extension;
|
|
if (StaticPrefs::clipboard_copy_image_file_as_png()) {
|
|
extension = ".png"_ns;
|
|
rv = imgTools->EncodeImage(image, nsLiteralCString(IMAGE_PNG), u""_ns,
|
|
getter_AddRefs(inputStream));
|
|
} else {
|
|
extension = ".bmp"_ns;
|
|
rv = imgTools->EncodeImage(image, nsLiteralCString(IMAGE_BMP),
|
|
u"bpp=32;version=3"_ns,
|
|
getter_AddRefs(inputStream));
|
|
}
|
|
if (NS_FAILED(rv) || !inputStream) {
|
|
return E_FAIL;
|
|
}
|
|
|
|
nsCOMPtr<imgIEncoder> encoder = do_QueryInterface(inputStream);
|
|
if (!encoder) {
|
|
return E_FAIL;
|
|
}
|
|
|
|
uint32_t size = 0;
|
|
rv = encoder->GetImageBufferUsed(&size);
|
|
if (NS_FAILED(rv)) {
|
|
return E_FAIL;
|
|
}
|
|
|
|
char* src = nullptr;
|
|
rv = encoder->GetImageBuffer(&src);
|
|
if (NS_FAILED(rv) || !src) {
|
|
return E_FAIL;
|
|
}
|
|
|
|
// Save the bitmap to a temporary location.
|
|
nsCOMPtr<nsIFile> dropFile;
|
|
rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(dropFile));
|
|
if (!dropFile) {
|
|
return E_FAIL;
|
|
}
|
|
|
|
// Filename must be random so as not to confuse apps like
|
|
// Photoshop which handle multiple drags into a single window.
|
|
char buf[9];
|
|
NS_MakeRandomString(buf, 8);
|
|
nsCString filename{buf};
|
|
filename.Append(extension);
|
|
dropFile->AppendNative(filename);
|
|
rv = dropFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0660);
|
|
if (NS_FAILED(rv)) {
|
|
return E_FAIL;
|
|
}
|
|
|
|
// Cache the temp file so we can delete it later and so
|
|
// it doesn't get recreated over and over on multiple calls
|
|
// which does occur from windows shell.
|
|
dropFile->Clone(getter_AddRefs(mCachedTempFile));
|
|
|
|
// Write the data to disk.
|
|
nsCOMPtr<nsIOutputStream> outStream;
|
|
rv = NS_NewLocalFileOutputStream(getter_AddRefs(outStream), dropFile);
|
|
if (NS_FAILED(rv)) {
|
|
return E_FAIL;
|
|
}
|
|
|
|
uint32_t written = 0;
|
|
rv = outStream->Write(src, size, &written);
|
|
if (NS_FAILED(rv) || written != size) {
|
|
return E_FAIL;
|
|
}
|
|
|
|
outStream->Close();
|
|
}
|
|
|
|
// Pass the file name back to the drop target so that it can access the file.
|
|
nsAutoString path;
|
|
rv = mCachedTempFile->GetPath(path);
|
|
if (NS_FAILED(rv)) return E_FAIL;
|
|
|
|
return AssignDropfile(aSTG, path);
|
|
}
|
|
|
|
HRESULT nsDataObj::DropTempFile(FORMATETC& aFE, STGMEDIUM& aSTG) {
|
|
nsresult rv;
|
|
if (!mCachedTempFile) {
|
|
// Tempfile will need a temporary location.
|
|
nsCOMPtr<nsIFile> dropFile;
|
|
rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(dropFile));
|
|
if (!dropFile) return E_FAIL;
|
|
|
|
// Filename must be random
|
|
nsCString filename;
|
|
nsAutoString wideFileName;
|
|
nsCOMPtr<nsIURI> sourceURI;
|
|
HRESULT res;
|
|
res = GetDownloadDetails(getter_AddRefs(sourceURI), wideFileName);
|
|
if (FAILED(res)) return res;
|
|
NS_CopyUnicodeToNative(wideFileName, filename);
|
|
|
|
dropFile->AppendNative(filename);
|
|
rv = dropFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0660);
|
|
if (NS_FAILED(rv)) return E_FAIL;
|
|
|
|
// Cache the temp file so we can delete it later and so
|
|
// it doesn't get recreated over and over on multiple calls
|
|
// which does occur from windows shell.
|
|
dropFile->Clone(getter_AddRefs(mCachedTempFile));
|
|
|
|
// Write the data to disk.
|
|
nsCOMPtr<nsIOutputStream> outStream;
|
|
rv = NS_NewLocalFileOutputStream(getter_AddRefs(outStream), dropFile);
|
|
if (NS_FAILED(rv)) return E_FAIL;
|
|
|
|
IStream* pStream = nullptr;
|
|
nsDataObj::CreateStream(&pStream);
|
|
NS_ENSURE_TRUE(pStream, E_FAIL);
|
|
|
|
char buffer[512];
|
|
ULONG readCount = 0;
|
|
uint32_t writeCount = 0;
|
|
while (true) {
|
|
HRESULT hres = pStream->Read(buffer, sizeof(buffer), &readCount);
|
|
if (FAILED(hres)) return E_FAIL;
|
|
if (readCount == 0) break;
|
|
rv = outStream->Write(buffer, readCount, &writeCount);
|
|
if (NS_FAILED(rv)) return E_FAIL;
|
|
}
|
|
outStream->Close();
|
|
pStream->Release();
|
|
}
|
|
|
|
// Pass the file name back to the drop target so that it can access the file.
|
|
nsAutoString path;
|
|
rv = mCachedTempFile->GetPath(path);
|
|
if (NS_FAILED(rv)) return E_FAIL;
|
|
|
|
return AssignDropfile(aSTG, path);
|
|
}
|
|
|
|
//-----------------------------------------------------
|
|
// Registers the DataFlavor/FE pair.
|
|
//-----------------------------------------------------
|
|
void nsDataObj::AddDataFlavor(const char* aDataFlavor, LPFORMATETC aFE) {
|
|
// These two lists are the mapping to and from data flavors and FEs.
|
|
// Later, OLE will tell us it needs a certain type of FORMATETC (text,
|
|
// unicode, etc) unicode, etc), so we will look up the data flavor that
|
|
// corresponds to the FE and then ask the transferable for that type of data.
|
|
mDataFlavors.AppendElement(aDataFlavor);
|
|
m_enumFE->AddFormatEtc(aFE);
|
|
}
|
|
|
|
//-----------------------------------------------------
|
|
// Sets the transferable object
|
|
//-----------------------------------------------------
|
|
void nsDataObj::SetTransferable(nsITransferable* aTransferable) {
|
|
mTransferable = aTransferable;
|
|
}
|
|
|
|
//
|
|
// ExtractURL
|
|
//
|
|
// Roots around in the transferable for the appropriate flavor that indicates
|
|
// a url and pulls out the url portion of the data. Used mostly for creating
|
|
// internet shortcuts on the desktop. The url flavor is of the format:
|
|
//
|
|
// <url> <linefeed> <page title>
|
|
//
|
|
nsresult nsDataObj ::ExtractShortcutURL(nsString& outURL) {
|
|
NS_ASSERTION(mTransferable, "We don't have a good transferable");
|
|
nsresult rv = NS_ERROR_FAILURE;
|
|
|
|
nsCOMPtr<nsISupports> genericURL;
|
|
if (NS_SUCCEEDED(mTransferable->GetTransferData(
|
|
kURLMime, getter_AddRefs(genericURL)))) {
|
|
nsCOMPtr<nsISupportsString> urlObject(do_QueryInterface(genericURL));
|
|
if (urlObject) {
|
|
nsAutoString url;
|
|
urlObject->GetData(url);
|
|
outURL = url;
|
|
|
|
// find the first linefeed in the data, that's where the url ends. trunc
|
|
// the result string at that point.
|
|
int32_t lineIndex = outURL.FindChar('\n');
|
|
NS_ASSERTION(lineIndex > 0,
|
|
"Format for url flavor is <url> <linefeed> <page title>");
|
|
if (lineIndex > 0) {
|
|
outURL.Truncate(lineIndex);
|
|
rv = NS_OK;
|
|
}
|
|
}
|
|
} else if (NS_SUCCEEDED(mTransferable->GetTransferData(
|
|
kURLDataMime, getter_AddRefs(genericURL))) ||
|
|
NS_SUCCEEDED(mTransferable->GetTransferData(
|
|
kURLPrivateMime, getter_AddRefs(genericURL)))) {
|
|
nsCOMPtr<nsISupportsString> urlObject(do_QueryInterface(genericURL));
|
|
if (urlObject) {
|
|
nsAutoString url;
|
|
urlObject->GetData(url);
|
|
outURL = url;
|
|
|
|
rv = NS_OK;
|
|
}
|
|
|
|
} // if found flavor
|
|
|
|
return rv;
|
|
|
|
} // ExtractShortcutURL
|
|
|
|
//
|
|
// ExtractShortcutTitle
|
|
//
|
|
// Roots around in the transferable for the appropriate flavor that indicates
|
|
// a url and pulls out the title portion of the data. Used mostly for creating
|
|
// internet shortcuts on the desktop. The url flavor is of the format:
|
|
//
|
|
// <url> <linefeed> <page title>
|
|
//
|
|
nsresult nsDataObj ::ExtractShortcutTitle(nsString& outTitle) {
|
|
NS_ASSERTION(mTransferable, "We'd don't have a good transferable");
|
|
nsresult rv = NS_ERROR_FAILURE;
|
|
|
|
nsCOMPtr<nsISupports> genericURL;
|
|
if (NS_SUCCEEDED(mTransferable->GetTransferData(
|
|
kURLMime, getter_AddRefs(genericURL)))) {
|
|
nsCOMPtr<nsISupportsString> urlObject(do_QueryInterface(genericURL));
|
|
if (urlObject) {
|
|
nsAutoString url;
|
|
urlObject->GetData(url);
|
|
|
|
// find the first linefeed in the data, that's where the url ends. we want
|
|
// everything after that linefeed. FindChar() returns -1 if we can't find
|
|
int32_t lineIndex = url.FindChar('\n');
|
|
NS_ASSERTION(lineIndex != -1,
|
|
"Format for url flavor is <url> <linefeed> <page title>");
|
|
if (lineIndex != -1) {
|
|
url.Mid(outTitle, lineIndex + 1, url.Length() - (lineIndex + 1));
|
|
rv = NS_OK;
|
|
}
|
|
}
|
|
} // if found flavor
|
|
|
|
return rv;
|
|
|
|
} // ExtractShortcutTitle
|
|
|
|
//
|
|
// BuildPlatformHTML
|
|
//
|
|
// Munge our HTML data to win32's CF_HTML spec. Basically, put the requisite
|
|
// header information on it. This will null-terminate |outPlatformHTML|. See
|
|
// https://docs.microsoft.com/en-us/windows/win32/dataxchg/html-clipboard-format
|
|
// for details.
|
|
//
|
|
// We assume that |inOurHTML| is already a fragment (ie, doesn't have <HTML>
|
|
// or <BODY> tags). We'll wrap the fragment with them to make other apps
|
|
// happy.
|
|
//
|
|
nsresult nsDataObj ::BuildPlatformHTML(const char* inOurHTML,
|
|
char** outPlatformHTML) {
|
|
*outPlatformHTML = nullptr;
|
|
nsDependentCString inHTMLString(inOurHTML);
|
|
|
|
// Do we already have mSourceURL from a drag?
|
|
if (mSourceURL.IsEmpty()) {
|
|
nsAutoString url;
|
|
ExtractShortcutURL(url);
|
|
|
|
AppendUTF16toUTF8(url, mSourceURL);
|
|
}
|
|
|
|
constexpr auto kStartHTMLPrefix = "Version:0.9\r\nStartHTML:"_ns;
|
|
constexpr auto kEndHTMLPrefix = "\r\nEndHTML:"_ns;
|
|
constexpr auto kStartFragPrefix = "\r\nStartFragment:"_ns;
|
|
constexpr auto kEndFragPrefix = "\r\nEndFragment:"_ns;
|
|
constexpr auto kStartSourceURLPrefix = "\r\nSourceURL:"_ns;
|
|
constexpr auto kEndFragTrailer = "\r\n"_ns;
|
|
|
|
// The CF_HTML's size is embedded in the fragment, in such a way that the
|
|
// number of digits in the size is part of the size itself. While it _is_
|
|
// technically possible to compute the necessary size of the size-field
|
|
// precisely -- by trial and error, if nothing else -- it's simpler just to
|
|
// pick a rough but generous estimate and zero-pad it. (Zero-padding is
|
|
// explicitly permitted by the format definition.)
|
|
//
|
|
// Originally, in 2001, the "rough but generous estimate" was 8 digits. While
|
|
// a maximum size of (10**9 - 1) bytes probably would have covered all
|
|
// possible use-cases at the time, it's somewhat more likely to overflow
|
|
// nowadays. Nonetheless, for the sake of backwards compatibility with any
|
|
// misbehaving consumers of our existing CF_HTML output, we retain exactly
|
|
// that padding for (most) fragments where it suffices. (No such misbehaving
|
|
// consumers are actually known, so this is arguably paranoia.)
|
|
//
|
|
// It is now 2022. A padding size of 16 will cover up to about 8.8 petabytes,
|
|
// which should be enough for at least the next few years or so.
|
|
const size_t numberLength = inHTMLString.Length() < 9999'0000 ? 8 : 16;
|
|
|
|
const size_t sourceURLLength = mSourceURL.Length();
|
|
|
|
const size_t fixedHeaderLen =
|
|
kStartHTMLPrefix.Length() + kEndHTMLPrefix.Length() +
|
|
kStartFragPrefix.Length() + kEndFragPrefix.Length() +
|
|
kEndFragTrailer.Length() + (4 * numberLength);
|
|
|
|
const size_t totalHeaderLen =
|
|
fixedHeaderLen + (sourceURLLength > 0
|
|
? kStartSourceURLPrefix.Length() + sourceURLLength
|
|
: 0);
|
|
|
|
constexpr auto kHeaderString = "<html><body>\r\n<!--StartFragment-->"_ns;
|
|
constexpr auto kTrailingString =
|
|
"<!--EndFragment-->\r\n"
|
|
"</body>\r\n"
|
|
"</html>"_ns;
|
|
|
|
// calculate the offsets
|
|
size_t startHTMLOffset = totalHeaderLen;
|
|
size_t startFragOffset = startHTMLOffset + kHeaderString.Length();
|
|
|
|
size_t endFragOffset = startFragOffset + inHTMLString.Length();
|
|
size_t endHTMLOffset = endFragOffset + kTrailingString.Length();
|
|
|
|
// now build the final version
|
|
nsCString clipboardString;
|
|
clipboardString.SetCapacity(endHTMLOffset);
|
|
|
|
const int numberLengthInt = static_cast<int>(numberLength);
|
|
clipboardString.Append(kStartHTMLPrefix);
|
|
clipboardString.AppendPrintf("%0*zu", numberLengthInt, startHTMLOffset);
|
|
|
|
clipboardString.Append(kEndHTMLPrefix);
|
|
clipboardString.AppendPrintf("%0*zu", numberLengthInt, endHTMLOffset);
|
|
|
|
clipboardString.Append(kStartFragPrefix);
|
|
clipboardString.AppendPrintf("%0*zu", numberLengthInt, startFragOffset);
|
|
|
|
clipboardString.Append(kEndFragPrefix);
|
|
clipboardString.AppendPrintf("%0*zu", numberLengthInt, endFragOffset);
|
|
|
|
if (sourceURLLength > 0) {
|
|
clipboardString.Append(kStartSourceURLPrefix);
|
|
clipboardString.Append(mSourceURL);
|
|
}
|
|
|
|
clipboardString.Append(kEndFragTrailer);
|
|
|
|
// Assert that the positional values were correct as we pass by their
|
|
// corresponding positions.
|
|
MOZ_ASSERT(clipboardString.Length() == startHTMLOffset);
|
|
clipboardString.Append(kHeaderString);
|
|
MOZ_ASSERT(clipboardString.Length() == startFragOffset);
|
|
clipboardString.Append(inHTMLString);
|
|
MOZ_ASSERT(clipboardString.Length() == endFragOffset);
|
|
clipboardString.Append(kTrailingString);
|
|
MOZ_ASSERT(clipboardString.Length() == endHTMLOffset);
|
|
|
|
*outPlatformHTML = ToNewCString(clipboardString, mozilla::fallible);
|
|
if (!*outPlatformHTML) return NS_ERROR_OUT_OF_MEMORY;
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
HRESULT
|
|
nsDataObj ::GetUniformResourceLocator(FORMATETC& aFE, STGMEDIUM& aSTG,
|
|
bool aIsUnicode) {
|
|
HRESULT res = S_OK;
|
|
if (IsFlavourPresent(kURLMime)) {
|
|
if (aIsUnicode)
|
|
res = ExtractUniformResourceLocatorW(aFE, aSTG);
|
|
else
|
|
res = ExtractUniformResourceLocatorA(aFE, aSTG);
|
|
} else
|
|
NS_WARNING("Not yet implemented");
|
|
return res;
|
|
}
|
|
|
|
HRESULT
|
|
nsDataObj::ExtractUniformResourceLocatorA(FORMATETC& aFE, STGMEDIUM& aSTG) {
|
|
HRESULT result = S_OK;
|
|
|
|
nsAutoString url;
|
|
if (NS_FAILED(ExtractShortcutURL(url))) return E_OUTOFMEMORY;
|
|
|
|
NS_LossyConvertUTF16toASCII asciiUrl(url);
|
|
const int totalLen = asciiUrl.Length() + 1;
|
|
HGLOBAL hGlobalMemory = GlobalAlloc(GMEM_ZEROINIT | GMEM_SHARE, totalLen);
|
|
if (!hGlobalMemory) return E_OUTOFMEMORY;
|
|
|
|
char* contents = reinterpret_cast<char*>(GlobalLock(hGlobalMemory));
|
|
if (!contents) {
|
|
GlobalFree(hGlobalMemory);
|
|
return E_OUTOFMEMORY;
|
|
}
|
|
|
|
strcpy(contents, asciiUrl.get());
|
|
GlobalUnlock(hGlobalMemory);
|
|
aSTG.hGlobal = hGlobalMemory;
|
|
aSTG.tymed = TYMED_HGLOBAL;
|
|
|
|
return result;
|
|
}
|
|
|
|
HRESULT
|
|
nsDataObj::ExtractUniformResourceLocatorW(FORMATETC& aFE, STGMEDIUM& aSTG) {
|
|
HRESULT result = S_OK;
|
|
|
|
nsAutoString url;
|
|
if (NS_FAILED(ExtractShortcutURL(url))) return E_OUTOFMEMORY;
|
|
|
|
const int totalLen = (url.Length() + 1) * sizeof(char16_t);
|
|
HGLOBAL hGlobalMemory = GlobalAlloc(GMEM_ZEROINIT | GMEM_SHARE, totalLen);
|
|
if (!hGlobalMemory) return E_OUTOFMEMORY;
|
|
|
|
wchar_t* contents = reinterpret_cast<wchar_t*>(GlobalLock(hGlobalMemory));
|
|
if (!contents) {
|
|
GlobalFree(hGlobalMemory);
|
|
return E_OUTOFMEMORY;
|
|
}
|
|
|
|
wcscpy(contents, url.get());
|
|
GlobalUnlock(hGlobalMemory);
|
|
aSTG.hGlobal = hGlobalMemory;
|
|
aSTG.tymed = TYMED_HGLOBAL;
|
|
|
|
return result;
|
|
}
|
|
|
|
// Gets the filename from the kFilePromiseURLMime flavour
|
|
HRESULT nsDataObj::GetDownloadDetails(nsIURI** aSourceURI,
|
|
nsAString& aFilename) {
|
|
*aSourceURI = nullptr;
|
|
|
|
NS_ENSURE_TRUE(mTransferable, E_FAIL);
|
|
|
|
// get the URI from the kFilePromiseURLMime flavor
|
|
nsCOMPtr<nsISupports> urlPrimitive;
|
|
nsresult rv = mTransferable->GetTransferData(kFilePromiseURLMime,
|
|
getter_AddRefs(urlPrimitive));
|
|
NS_ENSURE_SUCCESS(rv, E_FAIL);
|
|
nsCOMPtr<nsISupportsString> srcUrlPrimitive = do_QueryInterface(urlPrimitive);
|
|
NS_ENSURE_TRUE(srcUrlPrimitive, E_FAIL);
|
|
|
|
nsAutoString srcUri;
|
|
srcUrlPrimitive->GetData(srcUri);
|
|
if (srcUri.IsEmpty()) return E_FAIL;
|
|
nsCOMPtr<nsIURI> sourceURI;
|
|
NS_NewURI(getter_AddRefs(sourceURI), srcUri);
|
|
|
|
nsAutoString srcFileName;
|
|
nsCOMPtr<nsISupports> fileNamePrimitive;
|
|
(void)mTransferable->GetTransferData(kFilePromiseDestFilename,
|
|
getter_AddRefs(fileNamePrimitive));
|
|
nsCOMPtr<nsISupportsString> srcFileNamePrimitive =
|
|
do_QueryInterface(fileNamePrimitive);
|
|
if (srcFileNamePrimitive) {
|
|
srcFileNamePrimitive->GetData(srcFileName);
|
|
} else {
|
|
nsCOMPtr<nsIURL> sourceURL = do_QueryInterface(sourceURI);
|
|
if (!sourceURL) return E_FAIL;
|
|
|
|
nsAutoCString urlFileName;
|
|
sourceURL->GetFileName(urlFileName);
|
|
NS_UnescapeURL(urlFileName);
|
|
CopyUTF8toUTF16(urlFileName, srcFileName);
|
|
}
|
|
|
|
// make the name safe for the filesystem
|
|
ValidateFilename(srcFileName, false);
|
|
if (srcFileName.IsEmpty()) return E_FAIL;
|
|
|
|
sourceURI.swap(*aSourceURI);
|
|
aFilename = srcFileName;
|
|
return S_OK;
|
|
}
|
|
|
|
HRESULT nsDataObj::GetFileDescriptor_IStreamA(FORMATETC& aFE, STGMEDIUM& aSTG) {
|
|
HGLOBAL fileGroupDescHandle =
|
|
::GlobalAlloc(GMEM_ZEROINIT | GMEM_SHARE, sizeof(FILEGROUPDESCRIPTORW));
|
|
NS_ENSURE_TRUE(fileGroupDescHandle, E_OUTOFMEMORY);
|
|
|
|
LPFILEGROUPDESCRIPTORA fileGroupDescA =
|
|
reinterpret_cast<LPFILEGROUPDESCRIPTORA>(GlobalLock(fileGroupDescHandle));
|
|
if (!fileGroupDescA) {
|
|
::GlobalFree(fileGroupDescHandle);
|
|
return E_OUTOFMEMORY;
|
|
}
|
|
|
|
nsAutoString wideFileName;
|
|
HRESULT res;
|
|
nsCOMPtr<nsIURI> sourceURI;
|
|
res = GetDownloadDetails(getter_AddRefs(sourceURI), wideFileName);
|
|
if (FAILED(res)) {
|
|
::GlobalFree(fileGroupDescHandle);
|
|
return res;
|
|
}
|
|
|
|
nsAutoCString nativeFileName;
|
|
NS_CopyUnicodeToNative(wideFileName, nativeFileName);
|
|
|
|
strncpy(fileGroupDescA->fgd[0].cFileName, nativeFileName.get(), MAX_PATH - 1);
|
|
fileGroupDescA->fgd[0].cFileName[MAX_PATH - 1] = '\0';
|
|
|
|
// one file in the file block
|
|
fileGroupDescA->cItems = 1;
|
|
fileGroupDescA->fgd[0].dwFlags = FD_PROGRESSUI;
|
|
|
|
GlobalUnlock(fileGroupDescHandle);
|
|
aSTG.hGlobal = fileGroupDescHandle;
|
|
aSTG.tymed = TYMED_HGLOBAL;
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
HRESULT nsDataObj::GetFileDescriptor_IStreamW(FORMATETC& aFE, STGMEDIUM& aSTG) {
|
|
HGLOBAL fileGroupDescHandle =
|
|
::GlobalAlloc(GMEM_ZEROINIT | GMEM_SHARE, sizeof(FILEGROUPDESCRIPTORW));
|
|
NS_ENSURE_TRUE(fileGroupDescHandle, E_OUTOFMEMORY);
|
|
|
|
LPFILEGROUPDESCRIPTORW fileGroupDescW =
|
|
reinterpret_cast<LPFILEGROUPDESCRIPTORW>(GlobalLock(fileGroupDescHandle));
|
|
if (!fileGroupDescW) {
|
|
::GlobalFree(fileGroupDescHandle);
|
|
return E_OUTOFMEMORY;
|
|
}
|
|
|
|
nsAutoString wideFileName;
|
|
HRESULT res;
|
|
nsCOMPtr<nsIURI> sourceURI;
|
|
res = GetDownloadDetails(getter_AddRefs(sourceURI), wideFileName);
|
|
if (FAILED(res)) {
|
|
::GlobalFree(fileGroupDescHandle);
|
|
return res;
|
|
}
|
|
|
|
wcsncpy(fileGroupDescW->fgd[0].cFileName, wideFileName.get(), MAX_PATH - 1);
|
|
fileGroupDescW->fgd[0].cFileName[MAX_PATH - 1] = '\0';
|
|
// one file in the file block
|
|
fileGroupDescW->cItems = 1;
|
|
fileGroupDescW->fgd[0].dwFlags = FD_PROGRESSUI;
|
|
|
|
GlobalUnlock(fileGroupDescHandle);
|
|
aSTG.hGlobal = fileGroupDescHandle;
|
|
aSTG.tymed = TYMED_HGLOBAL;
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
HRESULT nsDataObj::GetFileContents_IStream(FORMATETC& aFE, STGMEDIUM& aSTG) {
|
|
IStream* pStream = nullptr;
|
|
|
|
nsDataObj::CreateStream(&pStream);
|
|
NS_ENSURE_TRUE(pStream, E_FAIL);
|
|
|
|
aSTG.tymed = TYMED_ISTREAM;
|
|
aSTG.pstm = pStream;
|
|
aSTG.pUnkForRelease = nullptr;
|
|
|
|
return S_OK;
|
|
}
|