diff --git a/dom/events/Clipboard.cpp b/dom/events/Clipboard.cpp index 2db245ace955..c667d8236b2c 100644 --- a/dom/events/Clipboard.cpp +++ b/dom/events/Clipboard.cpp @@ -78,6 +78,162 @@ static bool MaybeCreateAndDispatchMozClipboardReadPasteEvent( Cancelable::eNo))); } +namespace { + +/** + * This is a base class for ClipboardGetCallbackForRead and + * ClipboardGetCallbackForReadText. + */ +class ClipboardGetCallback : public nsIAsyncClipboardGetCallback { + public: + explicit ClipboardGetCallback(RefPtr&& aPromise) + : mPromise(std::move(aPromise)) {} + + // nsIAsyncClipboardGetCallback + NS_IMETHOD OnError(nsresult aResult) override final { + MOZ_ASSERT(mPromise); + RefPtr p(std::move(mPromise)); + p->MaybeRejectWithNotAllowedError( + "Clipboard read operation is not allowed."); + return NS_OK; + } + + protected: + virtual ~ClipboardGetCallback() { MOZ_ASSERT(!mPromise); }; + + // Not cycle-collected, because it should be nulled when the request is + // answered, rejected or aborted. + RefPtr mPromise; +}; + +class ClipboardGetCallbackForRead final : public ClipboardGetCallback { + public: + explicit ClipboardGetCallbackForRead(nsIGlobalObject* aGlobal, + RefPtr&& aPromise) + : ClipboardGetCallback(std::move(aPromise)), mGlobal(aGlobal) {} + + // This object will never be held by a cycle-collected object, so it doesn't + // need to be cycle-collected despite holding alive cycle-collected objects. + NS_DECL_ISUPPORTS + + // nsIAsyncClipboardGetCallback + NS_IMETHOD OnSuccess( + nsIAsyncGetClipboardData* aAsyncGetClipboardData) override { + MOZ_ASSERT(mPromise); + MOZ_ASSERT(aAsyncGetClipboardData); + + nsTArray flavorList; + nsresult rv = aAsyncGetClipboardData->GetFlavorList(flavorList); + if (NS_FAILED(rv)) { + return OnError(rv); + } + + AutoTArray, 3> entries; + for (const auto& format : flavorList) { + auto entry = MakeRefPtr( + mGlobal, NS_ConvertUTF8toUTF16(format)); + entry->LoadDataFromSystemClipboard(aAsyncGetClipboardData); + entries.AppendElement(std::move(entry)); + } + + RefPtr p(std::move(mPromise)); + // We currently only support one clipboard item. + p->MaybeResolve( + AutoTArray, 1>{MakeRefPtr( + mGlobal, PresentationStyle::Unspecified, std::move(entries))}); + + return NS_OK; + } + + protected: + ~ClipboardGetCallbackForRead() = default; + + nsCOMPtr mGlobal; +}; + +NS_IMPL_ISUPPORTS(ClipboardGetCallbackForRead, nsIAsyncClipboardGetCallback) + +class ClipboardGetCallbackForReadText final + : public ClipboardGetCallback, + public nsIAsyncClipboardRequestCallback { + public: + explicit ClipboardGetCallbackForReadText(RefPtr&& aPromise) + : ClipboardGetCallback(std::move(aPromise)) {} + + // This object will never be held by a cycle-collected object, so it doesn't + // need to be cycle-collected despite holding alive cycle-collected objects. + NS_DECL_ISUPPORTS + + // nsIAsyncClipboardGetCallback + NS_IMETHOD OnSuccess( + nsIAsyncGetClipboardData* aAsyncGetClipboardData) override { + MOZ_ASSERT(mPromise); + MOZ_ASSERT(!mTransferable); + MOZ_ASSERT(aAsyncGetClipboardData); + + AutoTArray flavors; + nsresult rv = aAsyncGetClipboardData->GetFlavorList(flavors); + if (NS_FAILED(rv)) { + return OnError(rv); + } + + mTransferable = do_CreateInstance("@mozilla.org/widget/transferable;1"); + if (NS_WARN_IF(!mTransferable)) { + return OnError(NS_ERROR_UNEXPECTED); + } + + mTransferable->Init(nullptr); + mTransferable->AddDataFlavor(kTextMime); + if (!flavors.Contains(kTextMime)) { + return OnComplete(NS_OK); + } + + rv = aAsyncGetClipboardData->GetData(mTransferable, this); + if (NS_FAILED(rv)) { + return OnError(rv); + } + + return NS_OK; + } + + // nsIAsyncClipboardRequestCallback + NS_IMETHOD OnComplete(nsresult aResult) override { + MOZ_ASSERT(mPromise); + MOZ_ASSERT(mTransferable); + + if (NS_FAILED(aResult)) { + return OnError(aResult); + } + + nsAutoString str; + nsCOMPtr data; + nsresult rv = + mTransferable->GetTransferData(kTextMime, getter_AddRefs(data)); + if (!NS_WARN_IF(NS_FAILED(rv))) { + nsCOMPtr supportsstr = do_QueryInterface(data); + MOZ_ASSERT(supportsstr); + if (supportsstr) { + supportsstr->GetData(str); + } + } + + RefPtr p(std::move(mPromise)); + p->MaybeResolve(str); + + return NS_OK; + } + + protected: + ~ClipboardGetCallbackForReadText() = default; + + nsCOMPtr mTransferable; +}; + +NS_IMPL_ISUPPORTS(ClipboardGetCallbackForReadText, nsIAsyncClipboardGetCallback, + nsIAsyncClipboardRequestCallback) + +} // namespace + void Clipboard::ReadRequest::Answer() { RefPtr p(std::move(mPromise)); RefPtr owner(std::move(mOwner)); @@ -90,89 +246,30 @@ void Clipboard::ReadRequest::Answer() { return; } + RefPtr callback; switch (mType) { case ReadRequestType::eRead: { - clipboardService - ->AsyncHasDataMatchingFlavors( - // Mandatory data types defined in - // https://w3c.github.io/clipboard-apis/#mandatory-data-types-x - AutoTArray{nsDependentCString(kHTMLMime), - nsDependentCString(kTextMime), - nsDependentCString(kPNGImageMime)}, - nsIClipboard::kGlobalClipboard) - ->Then( - GetMainThreadSerialEventTarget(), __func__, - /* resolve */ - [owner, p](nsTArray formats) { - nsCOMPtr global = do_QueryInterface(owner); - if (NS_WARN_IF(!global)) { - p->MaybeReject(NS_ERROR_UNEXPECTED); - return; - } - - AutoTArray, 3> entries; - for (const auto& format : formats) { - nsCOMPtr trans = - do_CreateInstance("@mozilla.org/widget/transferable;1"); - if (NS_WARN_IF(!trans)) { - continue; - } - - trans->Init(nullptr); - trans->AddDataFlavor(format.get()); - - RefPtr entry = - MakeRefPtr( - global, NS_ConvertUTF8toUTF16(format)); - entry->LoadDataFromSystemClipboard(*trans); - entries.AppendElement(std::move(entry)); - } - - // We currently only support one clipboard item. - AutoTArray, 1> items; - items.AppendElement(MakeRefPtr( - global, PresentationStyle::Unspecified, - std::move(entries))); - - p->MaybeResolve(std::move(items)); - }, - /* reject */ - [p](nsresult rv) { p->MaybeReject(rv); }); - break; - } - case ReadRequestType::eReadText: { - nsCOMPtr trans = - do_CreateInstance("@mozilla.org/widget/transferable;1"); - if (NS_WARN_IF(!trans)) { + nsCOMPtr global = do_QueryInterface(owner); + if (NS_WARN_IF(!global)) { p->MaybeReject(NS_ERROR_UNEXPECTED); return; } - trans->Init(nullptr); - trans->AddDataFlavor(kTextMime); - clipboardService->AsyncGetData(trans, nsIClipboard::kGlobalClipboard) - ->Then( - GetMainThreadSerialEventTarget(), __func__, - /* resolve */ - [trans, p]() { - nsCOMPtr data; - nsresult rv = - trans->GetTransferData(kTextMime, getter_AddRefs(data)); - - nsAutoString str; - if (!NS_WARN_IF(NS_FAILED(rv))) { - nsCOMPtr supportsstr = - do_QueryInterface(data); - MOZ_ASSERT(supportsstr); - if (supportsstr) { - supportsstr->GetData(str); - } - } - - p->MaybeResolve(str); - }, - /* reject */ - [p](nsresult rv) { p->MaybeReject(rv); }); + callback = MakeRefPtr(global, std::move(p)); + rv = clipboardService->AsyncGetData( + // Mandatory data types defined in + // https://w3c.github.io/clipboard-apis/#mandatory-data-types-x + AutoTArray{nsDependentCString(kHTMLMime), + nsDependentCString(kTextMime), + nsDependentCString(kPNGImageMime)}, + nsIClipboard::kGlobalClipboard, callback); + break; + } + case ReadRequestType::eReadText: { + callback = MakeRefPtr(std::move(p)); + rv = clipboardService->AsyncGetData( + AutoTArray{nsDependentCString(kTextMime)}, + nsIClipboard::kGlobalClipboard, callback); break; } default: { @@ -180,6 +277,12 @@ void Clipboard::ReadRequest::Answer() { break; } } + + if (NS_FAILED(rv)) { + MOZ_ASSERT(callback); + callback->OnError(rv); + return; + } } static bool IsReadTextExposedToContent() { @@ -755,7 +858,8 @@ already_AddRefed Clipboard::WriteText(const nsAString& aData, void Clipboard::ReadRequest::MaybeRejectWithNotAllowedError( const nsACString& aMessage) { - mPromise->MaybeRejectWithNotAllowedError(aMessage); + RefPtr p(std::move(mPromise)); + p->MaybeRejectWithNotAllowedError(aMessage); } void Clipboard::OnUserReactedToPasteMenuPopup(const bool aAllowed) { diff --git a/dom/events/ClipboardItem.cpp b/dom/events/ClipboardItem.cpp index 3226f408245c..76196be3b6b0 100644 --- a/dom/events/ClipboardItem.cpp +++ b/dom/events/ClipboardItem.cpp @@ -21,7 +21,8 @@ NS_IMPL_CYCLE_COLLECTION(ClipboardItem::ItemEntry, mGlobal, mData, mPendingGetTypeRequests) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ClipboardItem::ItemEntry) - NS_INTERFACE_MAP_ENTRY(nsISupports) + NS_INTERFACE_MAP_ENTRY(nsIAsyncClipboardRequestCallback) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, PromiseNativeHandler) NS_INTERFACE_MAP_END NS_IMPL_CYCLE_COLLECTING_ADDREF(ClipboardItem::ItemEntry) @@ -30,7 +31,7 @@ NS_IMPL_CYCLE_COLLECTING_RELEASE(ClipboardItem::ItemEntry) void ClipboardItem::ItemEntry::ResolvedCallback(JSContext* aCx, JS::Handle aValue, ErrorResult& aRv) { - MOZ_ASSERT(!mLoadingPromise.Exists()); + MOZ_ASSERT(!mTransferable); mIsLoadingData = false; OwningStringOrBlob clipboardData; if (!clipboardData.Init(aCx, aValue)) { @@ -45,7 +46,7 @@ void ClipboardItem::ItemEntry::ResolvedCallback(JSContext* aCx, void ClipboardItem::ItemEntry::RejectedCallback(JSContext* aCx, JS::Handle aValue, ErrorResult& aRv) { - MOZ_ASSERT(!mLoadingPromise.Exists()); + MOZ_ASSERT(!mTransferable); mIsLoadingData = false; RejectPendingPromises(NS_ERROR_DOM_DATA_ERR); } @@ -77,94 +78,95 @@ ClipboardItem::ItemEntry::GetData() { return GetDataPromise::CreateAndResolve(std::move(data), __func__); } +NS_IMETHODIMP ClipboardItem::ItemEntry::OnComplete(nsresult aResult) { + MOZ_ASSERT(mIsLoadingData); + + mIsLoadingData = false; + nsCOMPtr trans = std::move(mTransferable); + + if (NS_FAILED(aResult)) { + RejectPendingPromises(aResult); + return NS_OK; + } + + MOZ_ASSERT(trans); + nsCOMPtr data; + nsresult rv = trans->GetTransferData(NS_ConvertUTF16toUTF8(mType).get(), + getter_AddRefs(data)); + if (NS_WARN_IF(NS_FAILED(rv))) { + RejectPendingPromises(rv); + return NS_OK; + } + + RefPtr blob; + if (nsCOMPtr supportsstr = do_QueryInterface(data)) { + nsAutoString str; + supportsstr->GetData(str); + + blob = Blob::CreateStringBlob(mGlobal, NS_ConvertUTF16toUTF8(str), mType); + } else if (nsCOMPtr istream = do_QueryInterface(data)) { + uint64_t available; + void* data = nullptr; + rv = NS_ReadInputStreamToBuffer(istream, &data, -1, &available); + if (NS_WARN_IF(NS_FAILED(rv))) { + RejectPendingPromises(rv); + return NS_OK; + } + + blob = Blob::CreateMemoryBlob(mGlobal, data, available, mType); + } else if (nsCOMPtr supportscstr = + do_QueryInterface(data)) { + nsAutoCString str; + supportscstr->GetData(str); + + blob = Blob::CreateStringBlob(mGlobal, str, mType); + } + + if (!blob) { + RejectPendingPromises(NS_ERROR_DOM_DATA_ERR); + return NS_OK; + } + + OwningStringOrBlob clipboardData; + clipboardData.SetAsBlob() = std::move(blob); + MaybeResolvePendingPromises(std::move(clipboardData)); + return NS_OK; +} + void ClipboardItem::ItemEntry::LoadDataFromSystemClipboard( - nsITransferable& aTransferable) { + nsIAsyncGetClipboardData* aDataGetter) { + MOZ_ASSERT(aDataGetter); // XXX maybe we could consider adding a method to check whether the union // object is uninitialized or initialized. MOZ_DIAGNOSTIC_ASSERT(!mData.IsString() && !mData.IsBlob(), "Data should be uninitialized."); MOZ_DIAGNOSTIC_ASSERT(mLoadResult.isNothing(), "Should have no load result"); - MOZ_DIAGNOSTIC_ASSERT(!mIsLoadingData && !mLoadingPromise.Exists(), + MOZ_DIAGNOSTIC_ASSERT(!mIsLoadingData && !mTransferable, "Should not be in the process of loading data"); - nsresult rv; - nsCOMPtr clipboard( - do_GetService("@mozilla.org/widget/clipboard;1", &rv)); - if (NS_FAILED(rv)) { + mIsLoadingData = true; + + mTransferable = do_CreateInstance("@mozilla.org/widget/transferable;1"); + if (NS_WARN_IF(!mTransferable)) { + OnComplete(NS_ERROR_FAILURE); return; } - mIsLoadingData = true; - nsCOMPtr trans(&aTransferable); - clipboard->AsyncGetData(trans, nsIClipboard::kGlobalClipboard) - ->Then( - GetMainThreadSerialEventTarget(), __func__, - /* resolved */ - [self = RefPtr{this}, trans]() { - self->mIsLoadingData = false; - self->mLoadingPromise.Complete(); + mTransferable->Init(nullptr); + mTransferable->AddDataFlavor(NS_ConvertUTF16toUTF8(mType).get()); - nsCOMPtr data; - nsresult rv = trans->GetTransferData( - NS_ConvertUTF16toUTF8(self->Type()).get(), - getter_AddRefs(data)); - if (NS_WARN_IF(NS_FAILED(rv))) { - self->RejectPendingPromises(rv); - return; - } - - RefPtr blob; - if (nsCOMPtr supportsstr = - do_QueryInterface(data)) { - nsAutoString str; - supportsstr->GetData(str); - - blob = Blob::CreateStringBlob( - self->mGlobal, NS_ConvertUTF16toUTF8(str), self->Type()); - } else if (nsCOMPtr istream = - do_QueryInterface(data)) { - uint64_t available; - void* data = nullptr; - nsresult rv = - NS_ReadInputStreamToBuffer(istream, &data, -1, &available); - if (NS_WARN_IF(NS_FAILED(rv))) { - self->RejectPendingPromises(rv); - return; - } - - blob = Blob::CreateMemoryBlob(self->mGlobal, data, available, - self->Type()); - } else if (nsCOMPtr supportscstr = - do_QueryInterface(data)) { - nsAutoCString str; - supportscstr->GetData(str); - - blob = Blob::CreateStringBlob(self->mGlobal, str, self->Type()); - } - - if (!blob) { - self->RejectPendingPromises(NS_ERROR_DOM_DATA_ERR); - return; - } - - OwningStringOrBlob clipboardData; - clipboardData.SetAsBlob() = std::move(blob); - self->MaybeResolvePendingPromises(std::move(clipboardData)); - }, - /* rejected */ - [self = RefPtr{this}](nsresult rv) { - self->mIsLoadingData = false; - self->mLoadingPromise.Complete(); - self->RejectPendingPromises(rv); - }) - ->Track(mLoadingPromise); + nsresult rv = aDataGetter->GetData(mTransferable, this); + if (NS_FAILED(rv)) { + OnComplete(rv); + return; + } } void ClipboardItem::ItemEntry::LoadDataFromDataPromise(Promise& aDataPromise) { MOZ_DIAGNOSTIC_ASSERT(!mData.IsString() && !mData.IsBlob(), "Data should be uninitialized"); MOZ_DIAGNOSTIC_ASSERT(mLoadResult.isNothing(), "Should have no load result"); - MOZ_DIAGNOSTIC_ASSERT(!mIsLoadingData && !mLoadingPromise.Exists(), + MOZ_DIAGNOSTIC_ASSERT(!mIsLoadingData && !mTransferable, "Should not be in the process of loading data"); mIsLoadingData = true; @@ -221,7 +223,7 @@ void ClipboardItem::ItemEntry::RejectPendingPromises(nsresult aRv) { MOZ_DIAGNOSTIC_ASSERT(!mData.IsString() && !mData.IsBlob(), "Data should be uninitialized"); MOZ_DIAGNOSTIC_ASSERT(mLoadResult.isNothing(), "Should not have load result"); - MOZ_DIAGNOSTIC_ASSERT(!mIsLoadingData && !mLoadingPromise.Exists(), + MOZ_DIAGNOSTIC_ASSERT(!mIsLoadingData && !mTransferable, "Should not be in the process of loading data"); mLoadResult.emplace(aRv); auto promiseHolders = std::move(mPendingGetDataRequests); @@ -239,7 +241,7 @@ void ClipboardItem::ItemEntry::MaybeResolvePendingPromises( MOZ_DIAGNOSTIC_ASSERT(!mData.IsString() && !mData.IsBlob(), "Data should be uninitialized"); MOZ_DIAGNOSTIC_ASSERT(mLoadResult.isNothing(), "Should not have load result"); - MOZ_DIAGNOSTIC_ASSERT(!mIsLoadingData && !mLoadingPromise.Exists(), + MOZ_DIAGNOSTIC_ASSERT(!mIsLoadingData && !mTransferable, "Should not be in the process of loading data"); mLoadResult.emplace(NS_OK); mData = std::move(aData); diff --git a/dom/events/ClipboardItem.h b/dom/events/ClipboardItem.h index 95fdcd6ea9a0..9fe45bf93643 100644 --- a/dom/events/ClipboardItem.h +++ b/dom/events/ClipboardItem.h @@ -12,6 +12,7 @@ #include "mozilla/dom/PromiseNativeHandler.h" #include "mozilla/MozPromise.h" +#include "nsIClipboard.h" #include "nsWrapperCache.h" class nsITransferable; @@ -25,13 +26,15 @@ class Promise; class ClipboardItem final : public nsWrapperCache { public: - class ItemEntry final : public PromiseNativeHandler { + class ItemEntry final : public PromiseNativeHandler, + public nsIAsyncClipboardRequestCallback { public: using GetDataPromise = MozPromise; NS_DECL_CYCLE_COLLECTING_ISUPPORTS - NS_DECL_CYCLE_COLLECTION_CLASS(ItemEntry) + NS_DECL_NSIASYNCCLIPBOARDREQUESTCALLBACK + NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(ItemEntry, PromiseNativeHandler) explicit ItemEntry(nsIGlobalObject* aGlobal, const nsAString& aType) : mGlobal(aGlobal), mType(aType) { @@ -54,7 +57,7 @@ class ClipboardItem final : public nsWrapperCache { RefPtr GetData(); // Load data from system clipboard. - void LoadDataFromSystemClipboard(nsITransferable& aTransferable); + void LoadDataFromSystemClipboard(nsIAsyncGetClipboardData* aDataGetter); void LoadDataFromDataPromise(Promise& aDataPromise); // If clipboard data is in the process of loading from either system @@ -65,7 +68,6 @@ class ClipboardItem final : public nsWrapperCache { private: ~ItemEntry() { - mLoadingPromise.DisconnectIfExists(); if (!mPendingGetDataRequests.IsEmpty()) { RejectPendingPromises(NS_ERROR_FAILURE); } @@ -87,7 +89,7 @@ class ClipboardItem final : public nsWrapperCache { // Indicates if the data is still being loaded. bool mIsLoadingData = false; - MozPromiseRequestHolder mLoadingPromise; + nsCOMPtr mTransferable; // Pending promises for data retrieval requests. nsTArray> mPendingGetDataRequests; diff --git a/dom/ipc/ContentChild.cpp b/dom/ipc/ContentChild.cpp index 924f39de46c0..aa8ca2570164 100644 --- a/dom/ipc/ContentChild.cpp +++ b/dom/ipc/ContentChild.cpp @@ -24,6 +24,7 @@ #include "imgLoader.h" #include "ScrollingMetrics.h" #include "mozilla/BasePrincipal.h" +#include "mozilla/ClipboardReadRequestChild.h" #include "mozilla/Components.h" #include "mozilla/HangDetails.h" #include "mozilla/LoadInfo.h" @@ -1987,6 +1988,12 @@ PRemotePrintJobChild* ContentChild::AllocPRemotePrintJobChild() { #endif } +already_AddRefed +ContentChild::AllocPClipboardReadRequestChild( + const nsTArray& aTypes) { + return MakeAndAddRef(aTypes); +} + media::PMediaChild* ContentChild::AllocPMediaChild() { return media::AllocPMediaChild(); } diff --git a/dom/ipc/ContentChild.h b/dom/ipc/ContentChild.h index e1f4a23150c0..7e2b95184dbe 100644 --- a/dom/ipc/ContentChild.h +++ b/dom/ipc/ContentChild.h @@ -229,6 +229,9 @@ class ContentChild final : public PContentChild, PRemotePrintJobChild* AllocPRemotePrintJobChild(); + already_AddRefed AllocPClipboardReadRequestChild( + const nsTArray& aTypes); + PMediaChild* AllocPMediaChild(); bool DeallocPMediaChild(PMediaChild* aActor); diff --git a/dom/ipc/ContentParent.cpp b/dom/ipc/ContentParent.cpp index 53f717ee1028..811e693e9b84 100644 --- a/dom/ipc/ContentParent.cpp +++ b/dom/ipc/ContentParent.cpp @@ -53,6 +53,7 @@ #include "mozilla/BenchmarkStorageParent.h" #include "mozilla/Casting.h" #include "mozilla/ClearOnShutdown.h" +#include "mozilla/ClipboardReadRequestParent.h" #include "mozilla/ClipboardWriteRequestParent.h" #include "mozilla/ContentBlockingUserInteraction.h" #include "mozilla/FOGIPC.h" @@ -3563,26 +3564,6 @@ mozilla::ipc::IPCResult ContentParent::RecvClipboardHasType( return IPC_OK(); } -mozilla::ipc::IPCResult ContentParent::RecvClipboardHasTypesAsync( - nsTArray&& aTypes, const int32_t& aWhichClipboard, - ClipboardHasTypesAsyncResolver&& aResolver) { - nsresult rv; - nsCOMPtr clipboard(do_GetService(kCClipboardCID, &rv)); - if (NS_FAILED(rv)) { - return IPC_FAIL(this, "RecvGetClipboardTypes failed."); - } - - clipboard->AsyncHasDataMatchingFlavors(aTypes, aWhichClipboard) - ->Then( - GetMainThreadSerialEventTarget(), __func__, - /* resolve */ - [aResolver](nsTArray types) { aResolver(types); }, - /* reject */ - [aResolver](nsresult rv) { aResolver(nsTArray{}); }); - - return IPC_OK(); -} - mozilla::ipc::IPCResult ContentParent::RecvGetExternalClipboardFormats( const int32_t& aWhichClipboard, const bool& aPlainTextOnly, nsTArray* aTypes) { @@ -3592,6 +3573,54 @@ mozilla::ipc::IPCResult ContentParent::RecvGetExternalClipboardFormats( return IPC_OK(); } +namespace { + +class ClipboardGetCallback final : public nsIAsyncClipboardGetCallback { + public: + ClipboardGetCallback(ContentParent* aContentParent, + ContentParent::GetClipboardAsyncResolver&& aResolver) + : mContentParent(aContentParent), mResolver(std::move(aResolver)) {} + + // This object will never be held by a cycle-collected object, so it doesn't + // need to be cycle-collected despite holding alive cycle-collected objects. + NS_DECL_ISUPPORTS + + // nsIAsyncClipboardGetCallback + NS_IMETHOD OnSuccess( + nsIAsyncGetClipboardData* aAsyncGetClipboardData) override { + nsTArray flavors; + nsresult rv = aAsyncGetClipboardData->GetFlavorList(flavors); + if (NS_FAILED(rv)) { + return OnError(rv); + } + + auto requestParent = MakeNotNull>( + mContentParent, aAsyncGetClipboardData); + if (!mContentParent->SendPClipboardReadRequestConstructor( + requestParent, std::move(flavors))) { + return OnError(NS_ERROR_FAILURE); + } + + mResolver(PClipboardReadRequestOrError(requestParent)); + return NS_OK; + } + + NS_IMETHOD OnError(nsresult aResult) override { + mResolver(aResult); + return NS_OK; + } + + protected: + ~ClipboardGetCallback() = default; + + RefPtr mContentParent; + ContentParent::GetClipboardAsyncResolver mResolver; +}; + +NS_IMPL_ISUPPORTS(ClipboardGetCallback, nsIAsyncClipboardGetCallback) + +} // namespace + mozilla::ipc::IPCResult ContentParent::RecvGetClipboardAsync( nsTArray&& aTypes, const int32_t& aWhichClipboard, GetClipboardAsyncResolver&& aResolver) { @@ -3603,25 +3632,13 @@ mozilla::ipc::IPCResult ContentParent::RecvGetClipboardAsync( return IPC_OK(); } - // Create transferable - auto result = CreateTransferable(aTypes); - if (result.isErr()) { - aResolver(result.unwrapErr()); + auto callback = MakeRefPtr(this, std::move(aResolver)); + rv = clipboard->AsyncGetData(aTypes, aWhichClipboard, callback); + if (NS_FAILED(rv)) { + callback->OnError(rv); return IPC_OK(); } - // Get data from clipboard - nsCOMPtr trans = result.unwrap(); - clipboard->AsyncGetData(trans, nsIClipboard::kGlobalClipboard) - ->Then( - GetMainThreadSerialEventTarget(), __func__, - [trans, aResolver, - self = RefPtr{this}](GenericPromise::ResolveOrRejectValue&& aValue) { - IPCTransferableData ipcTransferableData; - nsContentUtils::TransferableToIPCTransferableData( - trans, &ipcTransferableData, false /* aInSyncMessage */, self); - aResolver(std::move(ipcTransferableData)); - }); return IPC_OK(); } diff --git a/dom/ipc/ContentParent.h b/dom/ipc/ContentParent.h index c3d44f57f468..97c1a13a0645 100644 --- a/dom/ipc/ContentParent.h +++ b/dom/ipc/ContentParent.h @@ -987,10 +987,6 @@ class ContentParent final : public PContentParent, const int32_t& aWhichClipboard, bool* aHasType); - mozilla::ipc::IPCResult RecvClipboardHasTypesAsync( - nsTArray&& aTypes, const int32_t& aWhichClipboard, - ClipboardHasTypesAsyncResolver&& aResolver); - mozilla::ipc::IPCResult RecvGetExternalClipboardFormats( const int32_t& aWhichClipboard, const bool& aPlainTextOnly, nsTArray* aTypes); diff --git a/dom/ipc/IPCTransferable.ipdlh b/dom/ipc/IPCTransferable.ipdlh index df82b7aa27a8..1e277d05dd7f 100644 --- a/dom/ipc/IPCTransferable.ipdlh +++ b/dom/ipc/IPCTransferable.ipdlh @@ -74,6 +74,11 @@ struct IPCTransferableData IPCTransferableDataItem[] items; }; +union IPCTransferableDataOrError { + IPCTransferableData; + nsresult; +}; + struct IPCTransferable { IPCTransferableData data; diff --git a/dom/ipc/PContent.ipdl b/dom/ipc/PContent.ipdl index 229f88c886eb..40ce960af8db 100644 --- a/dom/ipc/PContent.ipdl +++ b/dom/ipc/PContent.ipdl @@ -6,6 +6,7 @@ include protocol PBackgroundStarter; include protocol PBrowser; +include protocol PClipboardReadRequest; include protocol PClipboardWriteRequest; include protocol PCompositorManager; include protocol PContentPermissionRequest; @@ -461,8 +462,8 @@ struct IPCImage { ImageIntSize size; }; -union IPCTransferableDataOrError { - IPCTransferableData; +union PClipboardReadRequestOrError { + PClipboardReadRequest; nsresult; }; @@ -475,6 +476,7 @@ union IPCTransferableDataOrError { sync protocol PContent { manages PBrowser; + manages PClipboardReadRequest; manages PClipboardWriteRequest; manages PContentPermissionRequest; manages PCycleCollectWithLogs; @@ -1043,10 +1045,11 @@ child: // details. async InitNextGenLocalStorageEnabled(bool enabled); - async PRemotePrintJob(); + async PRemotePrintJob(); + + async PClipboardReadRequest(nsCString[] aTypes); parent: - async SynchronizeLayoutHistoryState(MaybeDiscardedBrowsingContext aContext, nullable nsILayoutHistoryState aState); @@ -1216,9 +1219,9 @@ parent: // Returns a list of formats supported by the clipboard sync GetExternalClipboardFormats(int32_t aWhichClipboard, bool aPlainTextOnly) returns (nsCString[] aTypes); - // Given a list of supported types, returns the clipboard data for the - // first type that matches. - async GetClipboardAsync(nsCString[] aTypes, int32_t aWhichClipboard) returns (IPCTransferableDataOrError transferableData); + // Requests getting data from clipboard. + async GetClipboardAsync(nsCString[] aTypes, int32_t aWhichClipboard) + returns (PClipboardReadRequestOrError aClipboardReadRequest); // Clears the clipboard. async EmptyClipboard(int32_t aWhichClipboard); @@ -1227,11 +1230,6 @@ parent: sync ClipboardHasType(nsCString[] aTypes, int32_t aWhichClipboard) returns (bool hasType); - // Given a list of supported types, returns a list of types that clipboard - // constains the data for the specified type. - async ClipboardHasTypesAsync(nsCString [] aTypes, int32_t aWhichClipboard) - returns (nsCString [] types); - /** * Notify the parent that the child has started a clipboard write request, * and that the data will be sent over another IPC message once it is ready. diff --git a/widget/ClipboardReadRequestChild.h b/widget/ClipboardReadRequestChild.h new file mode 100644 index 000000000000..81d82e62961d --- /dev/null +++ b/widget/ClipboardReadRequestChild.h @@ -0,0 +1,34 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef mozilla_ClipboardReadRequestChild_h +#define mozilla_ClipboardReadRequestChild_h + +#include "mozilla/PClipboardReadRequestChild.h" + +class nsITransferable; + +namespace mozilla { + +class ClipboardReadRequestChild final : public PClipboardReadRequestChild { + public: + explicit ClipboardReadRequestChild(const nsTArray& aFlavorList) { + mFlavorList.AppendElements(aFlavorList); + } + + NS_INLINE_DECL_REFCOUNTING(ClipboardReadRequestChild) + + const nsTArray& FlavorList() const { return mFlavorList; } + + protected: + virtual ~ClipboardReadRequestChild() = default; + + private: + nsTArray mFlavorList; +}; + +} // namespace mozilla + +#endif // mozilla_ClipboardReadRequestChild_h diff --git a/widget/ClipboardReadRequestParent.cpp b/widget/ClipboardReadRequestParent.cpp new file mode 100644 index 000000000000..3f045a800a66 --- /dev/null +++ b/widget/ClipboardReadRequestParent.cpp @@ -0,0 +1,115 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "mozilla/ClipboardReadRequestParent.h" + +#include "mozilla/dom/ContentParent.h" +#include "mozilla/net/CookieJarSettings.h" +#include "nsComponentManagerUtils.h" +#include "nsIClipboard.h" +#include "nsITransferable.h" +#include "nsWidgetsCID.h" + +using mozilla::dom::ContentParent; +using mozilla::ipc::IPCResult; + +namespace mozilla { + +namespace { + +class ClipboardGetDataCallback final : public nsIAsyncClipboardRequestCallback { + public: + explicit ClipboardGetDataCallback(std::function&& aCallback) + : mCallback(std::move(aCallback)) {} + + // This object will never be held by a cycle-collected object, so it doesn't + // need to be cycle-collected despite holding alive cycle-collected objects. + NS_DECL_ISUPPORTS + + // nsIAsyncClipboardRequestCallback + NS_IMETHOD OnComplete(nsresult aResult) override { + mCallback(aResult); + return NS_OK; + } + + protected: + ~ClipboardGetDataCallback() = default; + + std::function mCallback; +}; + +NS_IMPL_ISUPPORTS(ClipboardGetDataCallback, nsIAsyncClipboardRequestCallback) + +static Result, nsresult> CreateTransferable( + const nsTArray& aTypes) { + nsresult rv; + nsCOMPtr trans = + do_CreateInstance("@mozilla.org/widget/transferable;1", &rv); + if (NS_FAILED(rv)) { + return Err(rv); + } + + MOZ_TRY(trans->Init(nullptr)); + // The private flag is only used to prevent the data from being cached to the + // disk. The flag is not exported to the IPCDataTransfer object. + // The flag is set because we are not sure whether the clipboard data is used + // in a private browsing context. The transferable is only used in this scope, + // so the cache would not reduce memory consumption anyway. + trans->SetIsPrivateData(true); + // Fill out flavors for transferable + for (uint32_t t = 0; t < aTypes.Length(); t++) { + MOZ_TRY(trans->AddDataFlavor(aTypes[t].get())); + } + + return std::move(trans); +} + +} // namespace + +IPCResult ClipboardReadRequestParent::RecvGetData( + const nsTArray& aFlavors, GetDataResolver&& aResolver) { + bool valid = false; + if (NS_FAILED(mAsyncGetClipboardData->GetValid(&valid)) || !valid) { + Unused << PClipboardReadRequestParent::Send__delete__(this); + aResolver(NS_ERROR_FAILURE); + return IPC_OK(); + } + + // Create transferable + auto result = CreateTransferable(aFlavors); + if (result.isErr()) { + aResolver(result.unwrapErr()); + return IPC_OK(); + } + + nsCOMPtr trans = result.unwrap(); + RefPtr callback = + MakeRefPtr([self = RefPtr{this}, + resolver = std::move(aResolver), + trans, + manager = mManager](nsresult aRv) { + if (NS_FAILED(aRv)) { + bool valid = false; + if (NS_FAILED(self->mAsyncGetClipboardData->GetValid(&valid)) || + !valid) { + Unused << PClipboardReadRequestParent::Send__delete__(self); + } + resolver(aRv); + return; + } + + dom::IPCTransferableData ipcTransferableData; + nsContentUtils::TransferableToIPCTransferableData( + trans, &ipcTransferableData, false /* aInSyncMessage */, manager); + resolver(std::move(ipcTransferableData)); + }); + nsresult rv = mAsyncGetClipboardData->GetData(trans, callback); + if (NS_FAILED(rv)) { + callback->OnComplete(rv); + } + return IPC_OK(); +} + +} // namespace mozilla diff --git a/widget/ClipboardReadRequestParent.h b/widget/ClipboardReadRequestParent.h new file mode 100644 index 000000000000..cd51c4e3006d --- /dev/null +++ b/widget/ClipboardReadRequestParent.h @@ -0,0 +1,39 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef mozilla_ClipboardReadRequestParent_h +#define mozilla_ClipboardReadRequestParent_h + +#include "mozilla/dom/ContentParent.h" +#include "mozilla/PClipboardReadRequestParent.h" +#include "nsIClipboard.h" + +namespace mozilla { + +class ClipboardReadRequestParent final : public PClipboardReadRequestParent { + using IPCResult = mozilla::ipc::IPCResult; + using ContentParent = mozilla::dom::ContentParent; + + public: + ClipboardReadRequestParent(ContentParent* aManager, + nsIAsyncGetClipboardData* aAsyncGetClipboardData) + : mManager(aManager), mAsyncGetClipboardData(aAsyncGetClipboardData) {} + + NS_INLINE_DECL_REFCOUNTING(ClipboardReadRequestParent, override) + + // PClipboardReadRequestParent + IPCResult RecvGetData(const nsTArray& aFlavors, + GetDataResolver&& aResolver); + + private: + ~ClipboardReadRequestParent() = default; + + RefPtr mManager; + nsCOMPtr mAsyncGetClipboardData; +}; + +} // namespace mozilla + +#endif // mozilla_ClipboardReadRequestParent_h diff --git a/widget/PClipboardReadRequest.ipdl b/widget/PClipboardReadRequest.ipdl new file mode 100644 index 000000000000..39ae8ed38e0a --- /dev/null +++ b/widget/PClipboardReadRequest.ipdl @@ -0,0 +1,25 @@ +/* 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 protocol PContent; + +include DOMTypes; +include IPCTransferable; +include NeckoChannelParams; + +using nsContentPolicyType from "nsIContentPolicy.h"; + +namespace mozilla { + +protocol PClipboardReadRequest { + manager PContent; + + parent: + async GetData(nsCString[] aFlavors) returns (IPCTransferableDataOrError aTransferableData); + + both: + async __delete__(); +}; + +} // namespace mozilla diff --git a/widget/moz.build b/widget/moz.build index d40d03e5b8d4..87f5362f3b70 100644 --- a/widget/moz.build +++ b/widget/moz.build @@ -166,6 +166,8 @@ EXPORTS += [ EXPORTS.mozilla += [ "BasicEvents.h", + "ClipboardReadRequestChild.h", + "ClipboardReadRequestParent.h", "ClipboardWriteRequestChild.h", "ClipboardWriteRequestParent.h", "ColorScheme.h", @@ -215,6 +217,7 @@ EXPORTS.mozilla.widget += [ ] UNIFIED_SOURCES += [ + "ClipboardReadRequestParent.cpp", "ClipboardWriteRequestChild.cpp", "ClipboardWriteRequestParent.cpp", "CompositorWidget.cpp", @@ -374,6 +377,7 @@ else: IPDL_SOURCES += [ "LookAndFeelTypes.ipdlh", + "PClipboardReadRequest.ipdl", "PClipboardWriteRequest.ipdl", ] diff --git a/widget/nsBaseClipboard.cpp b/widget/nsBaseClipboard.cpp index b6defdcc5edf..2d672d0e3ec1 100644 --- a/widget/nsBaseClipboard.cpp +++ b/widget/nsBaseClipboard.cpp @@ -15,6 +15,8 @@ using mozilla::LogLevel; using mozilla::UniquePtr; using mozilla::dom::ClipboardCapabilities; +static const int32_t kGetAvailableFlavorsRetryCount = 5; + NS_IMPL_ISUPPORTS(nsBaseClipboard::AsyncSetClipboardData, nsIAsyncSetClipboardData) @@ -223,29 +225,7 @@ nsresult nsBaseClipboard::GetDataFromClipboardCache( return NS_ERROR_FAILURE; } - nsITransferable* cachedTransferable = clipboardCache->GetTransferable(); - MOZ_ASSERT(cachedTransferable); - - // get flavor list that includes all acceptable flavors (including ones - // obtained through conversion) - nsTArray flavors; - if (NS_FAILED(aTransferable->FlavorsTransferableCanImport(flavors))) { - return NS_ERROR_FAILURE; - } - - for (const auto& flavor : flavors) { - nsCOMPtr dataSupports; - if (NS_SUCCEEDED(cachedTransferable->GetTransferData( - flavor.get(), getter_AddRefs(dataSupports)))) { - MOZ_CLIPBOARD_LOG("%s: getting %s from cache.", __FUNCTION__, - flavor.get()); - aTransferable->SetTransferData(flavor.get(), dataSupports); - // maybe try to fill in more types? Is there a point? - return NS_OK; - } - } - - return NS_ERROR_FAILURE; + return clipboardCache->GetData(aTransferable); } /** @@ -277,41 +257,126 @@ NS_IMETHODIMP nsBaseClipboard::GetData(nsITransferable* aTransferable, return GetNativeClipboardData(aTransferable, aWhichClipboard); } -RefPtr nsBaseClipboard::AsyncGetData( - nsITransferable* aTransferable, int32_t aWhichClipboard) { +void nsBaseClipboard::MaybeRetryGetAvailableFlavors( + const nsTArray& aFlavorList, int32_t aWhichClipboard, + nsIAsyncClipboardGetCallback* aCallback, int32_t aRetryCount) { + // Note we have to get the clipboard sequence number first before the actual + // read. This is to use it to verify the clipboard data is still the one we + // try to read, instead of the later state. + auto sequenceNumberOrError = + GetNativeClipboardSequenceNumber(aWhichClipboard); + if (sequenceNumberOrError.isErr()) { + MOZ_CLIPBOARD_LOG("%s: unable to get sequence number for clipboard %d.", + __FUNCTION__, aWhichClipboard); + aCallback->OnError(sequenceNumberOrError.unwrapErr()); + return; + } + + int32_t sequenceNumber = sequenceNumberOrError.unwrap(); + AsyncHasNativeClipboardDataMatchingFlavors( + aFlavorList, aWhichClipboard, + [self = RefPtr{this}, callback = nsCOMPtr{aCallback}, aWhichClipboard, + aRetryCount, flavorList = aFlavorList.Clone(), + sequenceNumber](auto aFlavorsOrError) { + if (aFlavorsOrError.isErr()) { + MOZ_CLIPBOARD_LOG( + "%s: unable to get available flavors for clipboard %d.", + __FUNCTION__, aWhichClipboard); + callback->OnError(aFlavorsOrError.unwrapErr()); + return; + } + + auto sequenceNumberOrError = + self->GetNativeClipboardSequenceNumber(aWhichClipboard); + if (sequenceNumberOrError.isErr()) { + MOZ_CLIPBOARD_LOG( + "%s: unable to get sequence number for clipboard %d.", + __FUNCTION__, aWhichClipboard); + callback->OnError(sequenceNumberOrError.unwrapErr()); + return; + } + + if (sequenceNumber == sequenceNumberOrError.unwrap()) { + auto asyncGetClipboardData = + mozilla::MakeRefPtr( + aWhichClipboard, sequenceNumber, + std::move(aFlavorsOrError.unwrap()), false, self); + callback->OnSuccess(asyncGetClipboardData); + return; + } + + if (aRetryCount > 0) { + MOZ_CLIPBOARD_LOG( + "%s: clipboard=%d, ignore the data due to the sequence number " + "doesn't match, retry (%d) ..", + __FUNCTION__, aWhichClipboard, aRetryCount); + self->MaybeRetryGetAvailableFlavors(flavorList, aWhichClipboard, + callback, aRetryCount - 1); + return; + } + + MOZ_DIAGNOSTIC_ASSERT(false, "How can this happen?!?"); + callback->OnError(NS_ERROR_FAILURE); + }); +} + +NS_IMETHODIMP nsBaseClipboard::AsyncGetData( + const nsTArray& aFlavorList, int32_t aWhichClipboard, + nsIAsyncClipboardGetCallback* aCallback) { MOZ_CLIPBOARD_LOG("%s: clipboard=%d", __FUNCTION__, aWhichClipboard); - if (!aTransferable) { - NS_ASSERTION(false, "clipboard given a null transferable"); - return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); + if (!aCallback || aFlavorList.IsEmpty()) { + return NS_ERROR_INVALID_ARG; + } + + if (!nsIClipboard::IsClipboardTypeSupported(aWhichClipboard)) { + MOZ_CLIPBOARD_LOG("%s: clipboard %d is not supported.", __FUNCTION__, + aWhichClipboard); + return NS_ERROR_FAILURE; } if (mozilla::StaticPrefs::widget_clipboard_use_cached_data_enabled()) { // If we were the last ones to put something on the native clipboard, then // just use the cached transferable. Otherwise clear it because it isn't // relevant any more. - if (NS_SUCCEEDED( - GetDataFromClipboardCache(aTransferable, aWhichClipboard))) { - // maybe try to fill in more types? Is there a point? - return GenericPromise::CreateAndResolve(true, __func__); + if (auto* clipboardCache = GetClipboardCacheIfValid(aWhichClipboard)) { + nsITransferable* cachedTransferable = clipboardCache->GetTransferable(); + MOZ_ASSERT(cachedTransferable); + + nsTArray transferableFlavors; + if (NS_SUCCEEDED(cachedTransferable->FlavorsTransferableCanExport( + transferableFlavors))) { + nsTArray results; + for (const auto& transferableFlavor : transferableFlavors) { + for (const auto& flavor : aFlavorList) { + // XXX We need special check for image as we always put the + // image as "native" on the clipboard. + if (transferableFlavor.Equals(flavor) || + (transferableFlavor.Equals(kNativeImageMime) && + nsContentUtils::IsFlavorImage(flavor))) { + MOZ_CLIPBOARD_LOG(" has %s", flavor.get()); + results.AppendElement(flavor); + } + } + } + + // XXX Do we need to check system clipboard for the flavors that cannot + // be found in cache? + auto asyncGetClipboardData = mozilla::MakeRefPtr( + aWhichClipboard, clipboardCache->GetSequenceNumber(), + std::move(results), true, this); + aCallback->OnSuccess(asyncGetClipboardData); + return NS_OK; + } } - // at this point we can't satisfy the request from cache data so let's look - // for things other people put on the system clipboard + // At this point we can't satisfy the request from cache data so let's look + // for things other people put on the system clipboard. } - RefPtr dataPromise = - mozilla::MakeRefPtr(__func__); - AsyncGetNativeClipboardData(aTransferable, aWhichClipboard, - [dataPromise](nsresult aResult) { - if (NS_FAILED(aResult)) { - dataPromise->Reject(aResult, __func__); - return; - } - - dataPromise->Resolve(true, __func__); - }); - return dataPromise.forget(); + MaybeRetryGetAvailableFlavors(aFlavorList, aWhichClipboard, aCallback, + kGetAvailableFlavorsRetryCount); + return NS_OK; } NS_IMETHODIMP nsBaseClipboard::EmptyClipboard(int32_t aWhichClipboard) { @@ -415,52 +480,6 @@ nsBaseClipboard::HasDataMatchingFlavors(const nsTArray& aFlavorList, return NS_OK; } -RefPtr nsBaseClipboard::AsyncHasDataMatchingFlavors( - const nsTArray& aFlavorList, int32_t aWhichClipboard) { - MOZ_CLIPBOARD_LOG("%s: clipboard=%d", __FUNCTION__, aWhichClipboard); - if (MOZ_CLIPBOARD_LOG_ENABLED()) { - MOZ_CLIPBOARD_LOG(" Asking for content clipboard=%i:\n", - aWhichClipboard); - for (const auto& flavor : aFlavorList) { - MOZ_CLIPBOARD_LOG(" MIME %s", flavor.get()); - } - } - - if (mozilla::StaticPrefs::widget_clipboard_use_cached_data_enabled()) { - // First, check if we have valid data in our cached transferable. - auto flavorsOrError = GetFlavorsFromClipboardCache(aWhichClipboard); - if (flavorsOrError.isOk()) { - nsTArray results; - for (const auto& transferableFlavor : flavorsOrError.unwrap()) { - for (const auto& flavor : aFlavorList) { - // XXX We need special check for image as we always put the image as - // "native" on the clipboard. - if (transferableFlavor.Equals(flavor) || - (transferableFlavor.Equals(kNativeImageMime) && - nsContentUtils::IsFlavorImage(flavor))) { - MOZ_CLIPBOARD_LOG(" has %s", flavor.get()); - results.AppendElement(flavor); - } - } - } - return DataFlavorsPromise::CreateAndResolve(std::move(results), __func__); - } - } - - RefPtr flavorPromise = - mozilla::MakeRefPtr(__func__); - AsyncHasNativeClipboardDataMatchingFlavors( - aFlavorList, aWhichClipboard, [flavorPromise](auto aResultOrError) { - if (aResultOrError.isErr()) { - flavorPromise->Reject(aResultOrError.unwrapErr(), __func__); - return; - } - - flavorPromise->Resolve(std::move(aResultOrError.unwrap()), __func__); - }); - return flavorPromise.forget(); -} - NS_IMETHODIMP nsBaseClipboard::IsClipboardTypeSupported(int32_t aWhichClipboard, bool* aRetval) { @@ -520,6 +539,126 @@ void nsBaseClipboard::ClearClipboardCache(int32_t aClipboardType) { cache->Clear(); } +NS_IMPL_ISUPPORTS(nsBaseClipboard::AsyncGetClipboardData, + nsIAsyncGetClipboardData) + +nsBaseClipboard::AsyncGetClipboardData::AsyncGetClipboardData( + int32_t aClipboardType, int32_t aSequenceNumber, + nsTArray&& aFlavors, bool aFromCache, + nsBaseClipboard* aClipboard) + : mClipboardType(aClipboardType), + mSequenceNumber(aSequenceNumber), + mFlavors(std::move(aFlavors)), + mFromCache(aFromCache), + mClipboard(aClipboard) { + MOZ_ASSERT(mClipboard); + MOZ_ASSERT( + mClipboard->nsIClipboard::IsClipboardTypeSupported(mClipboardType)); +} + +NS_IMETHODIMP nsBaseClipboard::AsyncGetClipboardData::GetValid( + bool* aOutResult) { + *aOutResult = IsValid(); + return NS_OK; +} + +NS_IMETHODIMP nsBaseClipboard::AsyncGetClipboardData::GetFlavorList( + nsTArray& aFlavors) { + aFlavors.AppendElements(mFlavors); + return NS_OK; +} + +NS_IMETHODIMP nsBaseClipboard::AsyncGetClipboardData::GetData( + nsITransferable* aTransferable, + nsIAsyncClipboardRequestCallback* aCallback) { + MOZ_CLIPBOARD_LOG("AsyncGetClipboardData::GetData: %p", this); + + if (!aTransferable || !aCallback) { + return NS_ERROR_INVALID_ARG; + } + + nsTArray flavors; + nsresult rv = aTransferable->FlavorsTransferableCanImport(flavors); + if (NS_FAILED(rv)) { + return rv; + } + + // If the requested flavor is not in the list, throw an error. + for (const auto& flavor : flavors) { + if (!mFlavors.Contains(flavor)) { + return NS_ERROR_FAILURE; + } + } + + if (!IsValid()) { + aCallback->OnComplete(NS_ERROR_FAILURE); + return NS_OK; + } + + MOZ_ASSERT(mClipboard); + + if (mFromCache) { + const auto* clipboardCache = + mClipboard->GetClipboardCacheIfValid(mClipboardType); + // `IsValid()` above ensures we should get a valid cache and matched + // sequence number here. + MOZ_DIAGNOSTIC_ASSERT(clipboardCache); + MOZ_DIAGNOSTIC_ASSERT(clipboardCache->GetSequenceNumber() == + mSequenceNumber); + if (NS_SUCCEEDED(clipboardCache->GetData(aTransferable))) { + aCallback->OnComplete(NS_OK); + return NS_OK; + } + + // At this point we can't satisfy the request from cache data so let's look + // for things other people put on the system clipboard. + } + + // Since this is an async operation, we need to check if the data is still + // valid after we get the result. + mClipboard->AsyncGetNativeClipboardData( + aTransferable, mClipboardType, + [callback = nsCOMPtr{aCallback}, self = RefPtr{this}](nsresult aResult) { + // `IsValid()` checks the clipboard sequence number to ensure the data + // we are requesting is still valid. + callback->OnComplete(self->IsValid() ? aResult : NS_ERROR_FAILURE); + }); + return NS_OK; +} + +bool nsBaseClipboard::AsyncGetClipboardData::IsValid() { + if (!mClipboard) { + return false; + } + + // If the data should from cache, check if cache is still valid or the + // sequence numbers are matched. + if (mFromCache) { + const auto* clipboardCache = + mClipboard->GetClipboardCacheIfValid(mClipboardType); + if (!clipboardCache) { + mClipboard = nullptr; + return false; + } + + return mSequenceNumber == clipboardCache->GetSequenceNumber(); + } + + auto resultOrError = + mClipboard->GetNativeClipboardSequenceNumber(mClipboardType); + if (resultOrError.isErr()) { + mClipboard = nullptr; + return false; + } + + if (mSequenceNumber != resultOrError.unwrap()) { + mClipboard = nullptr; + return false; + } + + return true; +} + nsBaseClipboard::ClipboardCache* nsBaseClipboard::GetClipboardCacheIfValid( int32_t aClipboardType) { MOZ_ASSERT(nsIClipboard::IsClipboardTypeSupported(aClipboardType)); @@ -554,3 +693,34 @@ void nsBaseClipboard::ClipboardCache::Clear() { mTransferable = nullptr; mSequenceNumber = -1; } + +nsresult nsBaseClipboard::ClipboardCache::GetData( + nsITransferable* aTransferable) const { + MOZ_ASSERT(aTransferable); + MOZ_ASSERT(mozilla::StaticPrefs::widget_clipboard_use_cached_data_enabled()); + + // get flavor list that includes all acceptable flavors (including ones + // obtained through conversion) + nsTArray flavors; + if (NS_FAILED(aTransferable->FlavorsTransferableCanImport(flavors))) { + return NS_ERROR_FAILURE; + } + + MOZ_ASSERT(mTransferable); + for (const auto& flavor : flavors) { + nsCOMPtr dataSupports; + // XXX Maybe we need special check for image as we always put the image as + // "native" on the clipboard. + if (NS_SUCCEEDED(mTransferable->GetTransferData( + flavor.get(), getter_AddRefs(dataSupports)))) { + MOZ_CLIPBOARD_LOG("%s: getting %s from cache.", __FUNCTION__, + flavor.get()); + aTransferable->SetTransferData(flavor.get(), dataSupports); + // XXX we only read the first available type from native clipboard, so + // make cache behave the same. + return NS_OK; + } + } + + return NS_ERROR_FAILURE; +} diff --git a/widget/nsBaseClipboard.h b/widget/nsBaseClipboard.h index 7205d074faa1..e1cb9d552baa 100644 --- a/widget/nsBaseClipboard.h +++ b/widget/nsBaseClipboard.h @@ -44,17 +44,15 @@ class nsBaseClipboard : public nsIClipboard { nsIAsyncSetClipboardData** _retval) override final; NS_IMETHOD GetData(nsITransferable* aTransferable, int32_t aWhichClipboard) override final; + NS_IMETHOD AsyncGetData( + const nsTArray& aFlavorList, int32_t aWhichClipboard, + nsIAsyncClipboardGetCallback* aCallback) override final; NS_IMETHOD EmptyClipboard(int32_t aWhichClipboard) override final; NS_IMETHOD HasDataMatchingFlavors(const nsTArray& aFlavorList, int32_t aWhichClipboard, bool* aOutResult) override final; NS_IMETHOD IsClipboardTypeSupported(int32_t aWhichClipboard, bool* aRetval) override final; - RefPtr AsyncGetData( - nsITransferable* aTransferable, int32_t aWhichClipboard) override final; - RefPtr AsyncHasDataMatchingFlavors( - const nsTArray& aFlavorList, - int32_t aWhichClipboard) override final; using GetDataCallback = mozilla::MoveOnlyFunction; using HasMatchingFlavorsCallback = mozilla::MoveOnlyFunction mCallback; }; + class AsyncGetClipboardData final : public nsIAsyncGetClipboardData { + public: + AsyncGetClipboardData(int32_t aClipboardType, int32_t aSequenceNumber, + nsTArray&& aFlavors, bool aFromCache, + nsBaseClipboard* aClipboard); + + NS_DECL_ISUPPORTS + NS_DECL_NSIASYNCGETCLIPBOARDDATA + + private: + virtual ~AsyncGetClipboardData() = default; + bool IsValid(); + + // The clipboard type defined in nsIClipboard. + const int32_t mClipboardType; + // The sequence number associated with the clipboard content for this + // request. If it doesn't match with the current sequence number in system + // clipboard, this request targets stale data and is deemed invalid. + const int32_t mSequenceNumber; + // List of available data types for clipboard content. + const nsTArray mFlavors; + // Data should be read from cache. + const bool mFromCache; + // This is also used to indicate whether this request is still valid. + RefPtr mClipboard; + }; + class ClipboardCache final { public: ~ClipboardCache() { @@ -136,6 +161,7 @@ class nsBaseClipboard : public nsIClipboard { nsITransferable* GetTransferable() const { return mTransferable; } nsIClipboardOwner* GetClipboardOwner() const { return mClipboardOwner; } int32_t GetSequenceNumber() const { return mSequenceNumber; } + nsresult GetData(nsITransferable* aTransferable) const; private: nsCOMPtr mTransferable; @@ -143,6 +169,11 @@ class nsBaseClipboard : public nsIClipboard { int32_t mSequenceNumber = -1; }; + void MaybeRetryGetAvailableFlavors(const nsTArray& aFlavorList, + int32_t aWhichClipboard, + nsIAsyncClipboardGetCallback* aCallback, + int32_t aRetryCount); + // Return clipboard cache if the cached data is valid, otherwise clear the // cached data and returns null. ClipboardCache* GetClipboardCacheIfValid(int32_t aClipboardType); diff --git a/widget/nsClipboardProxy.cpp b/widget/nsClipboardProxy.cpp index aa489847145b..a350a98dad2d 100644 --- a/widget/nsClipboardProxy.cpp +++ b/widget/nsClipboardProxy.cpp @@ -7,12 +7,14 @@ #if defined(ACCESSIBILITY) && defined(XP_WIN) # include "mozilla/a11y/Compatibility.h" #endif +#include "mozilla/ClipboardReadRequestChild.h" #include "mozilla/ClipboardWriteRequestChild.h" #include "mozilla/dom/ContentChild.h" #include "mozilla/net/CookieJarSettings.h" #include "mozilla/Maybe.h" #include "mozilla/Unused.h" #include "nsArrayUtils.h" +#include "nsBaseClipboard.h" #include "nsISupportsPrimitives.h" #include "nsCOMPtr.h" #include "nsComponentManagerUtils.h" @@ -67,6 +69,149 @@ nsClipboardProxy::GetData(nsITransferable* aTransferable, false /* aFilterUnknownFlavors */); } +namespace { + +class AsyncGetClipboardDataProxy final : public nsIAsyncGetClipboardData { + public: + explicit AsyncGetClipboardDataProxy(ClipboardReadRequestChild* aActor) + : mActor(aActor) { + MOZ_ASSERT(mActor); + } + + NS_DECL_ISUPPORTS + NS_DECL_NSIASYNCGETCLIPBOARDDATA + + private: + virtual ~AsyncGetClipboardDataProxy() { + MOZ_ASSERT(mActor); + if (mActor->CanSend()) { + PClipboardReadRequestChild::Send__delete__(mActor); + } + }; + + RefPtr mActor; +}; + +NS_IMPL_ISUPPORTS(AsyncGetClipboardDataProxy, nsIAsyncGetClipboardData) + +NS_IMETHODIMP AsyncGetClipboardDataProxy::GetValid(bool* aOutResult) { + MOZ_ASSERT(mActor); + *aOutResult = mActor->CanSend(); + return NS_OK; +} + +NS_IMETHODIMP AsyncGetClipboardDataProxy::GetFlavorList( + nsTArray& aFlavorList) { + MOZ_ASSERT(mActor); + aFlavorList.AppendElements(mActor->FlavorList()); + return NS_OK; +} + +NS_IMETHODIMP AsyncGetClipboardDataProxy::GetData( + nsITransferable* aTransferable, + nsIAsyncClipboardRequestCallback* aCallback) { + if (!aTransferable || !aCallback) { + return NS_ERROR_INVALID_ARG; + } + + // Get a list of flavors this transferable can import + nsTArray flavors; + nsresult rv = aTransferable->FlavorsTransferableCanImport(flavors); + if (NS_FAILED(rv)) { + return rv; + } + + MOZ_ASSERT(mActor); + // If the requested flavor is not in the list, throw an error. + for (const auto& flavor : flavors) { + if (!mActor->FlavorList().Contains(flavor)) { + return NS_ERROR_FAILURE; + } + } + + if (!mActor->CanSend()) { + return aCallback->OnComplete(NS_ERROR_FAILURE); + } + + mActor->SendGetData(flavors)->Then( + GetMainThreadSerialEventTarget(), __func__, + /* resolve */ + [self = RefPtr{this}, callback = nsCOMPtr{aCallback}, + transferable = nsCOMPtr{aTransferable}]( + const IPCTransferableDataOrError& aIpcTransferableDataOrError) { + if (aIpcTransferableDataOrError.type() == + IPCTransferableDataOrError::Tnsresult) { + MOZ_ASSERT(NS_FAILED(aIpcTransferableDataOrError.get_nsresult())); + callback->OnComplete(aIpcTransferableDataOrError.get_nsresult()); + return; + } + + nsresult rv = nsContentUtils::IPCTransferableDataToTransferable( + aIpcTransferableDataOrError.get_IPCTransferableData(), + false /* aAddDataFlavor */, transferable, + false /* aFilterUnknownFlavors */); + if (NS_FAILED(rv)) { + callback->OnComplete(rv); + return; + } + + callback->OnComplete(NS_OK); + }, + /* reject */ + [callback = + nsCOMPtr{aCallback}](mozilla::ipc::ResponseRejectReason aReason) { + callback->OnComplete(NS_ERROR_FAILURE); + }); + + return NS_OK; +} + +} // namespace + +NS_IMETHODIMP nsClipboardProxy::AsyncGetData( + const nsTArray& aFlavorList, int32_t aWhichClipboard, + nsIAsyncClipboardGetCallback* aCallback) { + if (!aCallback || aFlavorList.IsEmpty()) { + return NS_ERROR_INVALID_ARG; + } + + if (!nsIClipboard::IsClipboardTypeSupported(aWhichClipboard)) { + MOZ_CLIPBOARD_LOG("%s: clipboard %d is not supported.", __FUNCTION__, + aWhichClipboard); + return NS_ERROR_FAILURE; + } + + ContentChild::GetSingleton() + ->SendGetClipboardAsync(aFlavorList, aWhichClipboard) + ->Then( + GetMainThreadSerialEventTarget(), __func__, + /* resolve */ + [callback = nsCOMPtr{aCallback}](const PClipboardReadRequestOrError& + aClipboardReadRequestOrError) { + if (aClipboardReadRequestOrError.type() == + PClipboardReadRequestOrError::Tnsresult) { + MOZ_ASSERT( + NS_FAILED(aClipboardReadRequestOrError.get_nsresult())); + callback->OnError(aClipboardReadRequestOrError.get_nsresult()); + return; + } + + auto asyncGetClipboardData = MakeRefPtr( + static_cast( + aClipboardReadRequestOrError.get_PClipboardReadRequest() + .AsChild() + .get())); + + callback->OnSuccess(asyncGetClipboardData); + }, + /* reject */ + [callback = nsCOMPtr{aCallback}]( + mozilla::ipc::ResponseRejectReason aReason) { + callback->OnError(NS_ERROR_FAILURE); + }); + return NS_OK; +} + NS_IMETHODIMP nsClipboardProxy::EmptyClipboard(int32_t aWhichClipboard) { ContentChild::GetSingleton()->SendEmptyClipboard(aWhichClipboard); @@ -112,70 +257,3 @@ void nsClipboardProxy::SetCapabilities( const ClipboardCapabilities& aClipboardCaps) { mClipboardCaps = aClipboardCaps; } - -RefPtr nsClipboardProxy::AsyncHasDataMatchingFlavors( - const nsTArray& aFlavorList, int32_t aWhichClipboard) { - auto promise = MakeRefPtr(__func__); - ContentChild::GetSingleton() - ->SendClipboardHasTypesAsync(aFlavorList, aWhichClipboard) - ->Then( - GetMainThreadSerialEventTarget(), __func__, - /* resolve */ - [promise](nsTArray types) { - promise->Resolve(std::move(types), __func__); - }, - /* reject */ - [promise](mozilla::ipc::ResponseRejectReason aReason) { - promise->Reject(NS_ERROR_FAILURE, __func__); - }); - - return promise.forget(); -} - -RefPtr nsClipboardProxy::AsyncGetData( - nsITransferable* aTransferable, int32_t aWhichClipboard) { - if (!aTransferable) { - return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__); - } - - // Get a list of flavors this transferable can import - nsTArray flavors; - nsresult rv = aTransferable->FlavorsTransferableCanImport(flavors); - if (NS_FAILED(rv)) { - return GenericPromise::CreateAndReject(rv, __func__); - } - - nsCOMPtr transferable(aTransferable); - auto promise = MakeRefPtr(__func__); - ContentChild::GetSingleton() - ->SendGetClipboardAsync(flavors, aWhichClipboard) - ->Then( - GetMainThreadSerialEventTarget(), __func__, - /* resolve */ - [promise, transferable]( - const IPCTransferableDataOrError& ipcTransferableDataOrError) { - if (ipcTransferableDataOrError.type() == - IPCTransferableDataOrError::Tnsresult) { - promise->Reject(ipcTransferableDataOrError.get_nsresult(), - __func__); - return; - } - - nsresult rv = nsContentUtils::IPCTransferableDataToTransferable( - ipcTransferableDataOrError.get_IPCTransferableData(), - false /* aAddDataFlavor */, transferable, - false /* aFilterUnknownFlavors */); - if (NS_FAILED(rv)) { - promise->Reject(rv, __func__); - return; - } - - promise->Resolve(true, __func__); - }, - /* reject */ - [promise](mozilla::ipc::ResponseRejectReason aReason) { - promise->Reject(NS_ERROR_FAILURE, __func__); - }); - - return promise.forget(); -} diff --git a/widget/nsIClipboard.idl b/widget/nsIClipboard.idl index cf920f1fde37..3980a89428ed 100644 --- a/widget/nsIClipboard.idl +++ b/widget/nsIClipboard.idl @@ -9,17 +9,8 @@ #include "nsITransferable.idl" #include "nsIClipboardOwner.idl" -%{C++ -#include "mozilla/MozPromise.h" - -using DataFlavorsPromise = mozilla::MozPromise, nsresult, true>; -%} - interface nsIArray; -native AsyncGetDataPromise(RefPtr); -native AsyncDataFlavorsPromise(RefPtr); - [scriptable, builtinclass, uuid(801e2318-c8fa-11ed-afa1-0242ac120002)] interface nsIAsyncSetClipboardData : nsISupports { /** @@ -55,6 +46,54 @@ interface nsIAsyncClipboardRequestCallback : nsISupports void onComplete(in nsresult aResult); }; +[scriptable, builtinclass, uuid(c18ea2f7-6b6f-4a38-9ab3-a8781fdfcc39)] +interface nsIAsyncGetClipboardData : nsISupports { + /** + * Determines whether this request is still valid (e.g., the clipboard content + * associated with this request is not stale). + */ + readonly attribute boolean valid; + + /** + * The available flavors in the clipboard. + */ + readonly attribute Array flavorList; + + /** + * Filters the flavors that `aTransferable` can import (see + * `nsITransferable::flavorsTransferableCanImport`). Every specified flavors + * must exist in `flavorList`, or the request will be rejected. If the request + * remains valid, it retrieves the data for the first flavor. The data is then + * set for `aTransferable`. + * + * @param aTransferable + * The transferable which contains the flavors to be read. + * @param aCallback + * The nsIAsyncClipboardRequestCallback to be invoked once the get + * request is either successfully completed or rejected. + * @result NS_OK if no errors + */ + void getData(in nsITransferable aTransferable, + in nsIAsyncClipboardRequestCallback aCallback); +}; + +[scriptable, uuid(ce23c1c4-58fd-4c33-8579-fa0796d9652c)] +interface nsIAsyncClipboardGetCallback : nsISupports +{ + /** + * Indicates that the clipboard get request has succeeded. + */ + void onSuccess(in nsIAsyncGetClipboardData aAsyncGetClipboardData); + + /** + * Indicates that the clipboard get request has rejected. + * + * @param aResult + * The reason for the rejection, can not be NS_OK. + */ + void onError(in nsresult aResult); +}; + [scriptable, builtinclass, uuid(ceaa0047-647f-4b8e-ad1c-aff9fa62aa51)] interface nsIClipboard : nsISupports { @@ -109,6 +148,26 @@ interface nsIClipboard : nsISupports void getData ( in nsITransferable aTransferable, in long aWhichClipboard ) ; + /** + * Requests getting data asynchronously from the native clipboard. This does + * not actually retreive the data, but returns a nsIAsyncGetClipboardData + * contains current avaiable data formats. If the native clipboard is + * updated, either by us or other application, the existing + * nsIAsyncGetClipboardData becomes invalid. + * + * @param aFlavorList + * Specific data formats ('flavors') that can be retrieved from the + * clipboard. + * @param aWhichClipboard + * Specifies the clipboard to which this operation applies. + * @param aCallback + * The callback object that will be notified upon completion. + * @result NS_OK if no errors + */ + void asyncGetData(in Array aFlavorList, + in long aWhichClipboard, + in nsIAsyncClipboardGetCallback aCallback); + /** * This empties the clipboard and notifies the clipboard owner. * This empties the "logical" clipboard. It does not clear the native clipboard. @@ -143,29 +202,4 @@ interface nsIClipboard : nsISupports */ [infallible] boolean isClipboardTypeSupported(in long aWhichClipboard); - - /** - * Filters the flavors aTransferable can import (see - * `nsITransferable::flavorsTransferableCanImport`) and gets the data for the - * first flavor. That data is set for aTransferable. - * - * @param aTransferable The transferable - * @param aWhichClipboard Specifies the clipboard to which this operation applies. - * @return MozPromise The returned promise will resolve when the data is ready or reject - * if any error occurs. - */ - [noscript, notxpcom, nostdcall] - AsyncGetDataPromise asyncGetData(in nsITransferable aTransferable, in long aWhichClipboard); - - /** - * Check if there is data on the clipboard matching each of the flavors in the - * given list. - * - * @param aFlavorList An array of ASCII strings. - * @param aWhichClipboard Specifies the clipboard to which this operation applies. - * @return MozPromise The returned promise will resolve with the list of matched flavors - * when the check is completed or reject if any error occurs. - */ - [noscript, notxpcom, nostdcall] - AsyncDataFlavorsPromise asyncHasDataMatchingFlavors(in Array aFlavorList, in long aWhichClipboard); }; diff --git a/widget/tests/chrome.toml b/widget/tests/chrome.toml index 840e477ea077..2e5c2682df6c 100644 --- a/widget/tests/chrome.toml +++ b/widget/tests/chrome.toml @@ -74,6 +74,9 @@ skip-if = ["toolkit != 'cocoa'"] # Cocoa widget test ["test_clipboard_chrome.html"] support-files = "file_test_clipboard.js" +["test_clipboard_asyncGetData_chrome.html"] +support-files = "file_test_clipboard_asyncGetData.js" + ["test_clipboard_asyncSetData_chrome.html"] support-files = "file_test_clipboard_asyncSetData.js" diff --git a/widget/tests/clipboard_helper.js b/widget/tests/clipboard_helper.js index 222872775553..1e882a2cb1ab 100644 --- a/widget/tests/clipboard_helper.js +++ b/widget/tests/clipboard_helper.js @@ -14,11 +14,23 @@ const clipboardTypes = [ clipboard.kSelectionCache, ]; +function emptyClipboardData(aType) { + // XXX gtk doesn't support emptying clipboard data which is stored from + // other application (bug 1853884). As a workaround, we set dummy data + // to the clipboard first to ensure the subsequent emptyClipboard call + // works. + if (navigator.platform.includes("Linux")) { + writeStringToClipboard("foo", "text/plain", aType); + } + + clipboard.emptyClipboard(aType); +} + function cleanupAllClipboard() { clipboardTypes.forEach(function (type) { if (clipboard.isClipboardTypeSupported(type)) { info(`cleanup clipboard ${type}`); - clipboard.emptyClipboard(type); + emptyClipboardData(type); } }); } @@ -53,6 +65,14 @@ function addStringToTransferable(aFlavor, aStr, aTrans) { aTrans.setTransferData(aFlavor, supportsStr); } +function updateStringToTransferable(aFlavor, aStr, aTrans) { + let supportsStr = Cc["@mozilla.org/supports-string;1"].createInstance( + Ci.nsISupportsString + ); + supportsStr.data = aStr; + aTrans.setTransferData(aFlavor, supportsStr); +} + function writeStringToClipboard( aStr, aFlavor, @@ -79,6 +99,23 @@ function writeStringToClipboard( } clipboard.setData(trans, aClipboardOwner, aClipboardType); + // XXX gtk doesn't support get empty text data from clipboard, bug 1852983. + if (aStr == "" && navigator.platform.includes("Linux")) { + todo_is( + getClipboardData(aFlavor, aClipboardType), + "", + `Should get empty string on clipboard type ${aClipboardType}` + ); + } else { + is( + getClipboardData(aFlavor, aClipboardType), + // On Windows, widget adds extra data into HTML clipboard. + aFlavor == "text/html" && navigator.platform.includes("Win") + ? `\n${aStr}\n\n` + : aStr, + "ensure clipboard data is set" + ); + } } function writeRandomStringToClipboard( @@ -115,3 +152,73 @@ function getClipboardData(aFlavor, aClipboardType) { return null; } } + +function asyncGetClipboardData(aClipboardType) { + return new Promise((resolve, reject) => { + try { + clipboard.asyncGetData( + ["text/plain", "text/html", "image/png"], + aClipboardType, + { + QueryInterface: SpecialPowers.ChromeUtils.generateQI([ + "nsIAsyncClipboardGetCallback", + ]), + // nsIAsyncClipboardGetCallback + onSuccess: SpecialPowers.wrapCallback(function ( + aAsyncGetClipboardData + ) { + resolve(aAsyncGetClipboardData); + }), + onError: SpecialPowers.wrapCallback(function (aResult) { + reject(aResult); + }), + } + ); + } catch (e) { + ok(false, `asyncGetData should not throw`); + reject(e); + } + }); +} + +function asyncClipboardRequestGetData(aRequest, aFlavor, aThrows = false) { + return new Promise((resolve, reject) => { + var trans = Cc["@mozilla.org/widget/transferable;1"].createInstance( + Ci.nsITransferable + ); + trans.init(null); + trans.addDataFlavor(aFlavor); + try { + aRequest.getData(trans, aResult => { + if (aResult != Cr.NS_OK) { + reject(aResult); + return; + } + + try { + var data = SpecialPowers.createBlankObject(); + trans.getTransferData(aFlavor, data); + resolve(data.value.QueryInterface(Ci.nsISupportsString).data); + } catch (ex) { + // XXX: should widget set empty string to transferable when there no + // data in system clipboard? + resolve(""); + } + }); + ok( + !aThrows, + `nsIAsyncGetClipboardData.getData should ${ + aThrows ? "throw" : "success" + }` + ); + } catch (e) { + ok( + aThrows, + `nsIAsyncGetClipboardData.getData should ${ + aThrows ? "throw" : "success" + }` + ); + reject(e); + } + }); +} diff --git a/widget/tests/file_test_clipboard_asyncGetData.js b/widget/tests/file_test_clipboard_asyncGetData.js new file mode 100644 index 000000000000..63bc07e359a2 --- /dev/null +++ b/widget/tests/file_test_clipboard_asyncGetData.js @@ -0,0 +1,164 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* import-globals-from clipboard_helper.js */ + +"use strict"; + +clipboardTypes.forEach(function (type) { + if (!clipboard.isClipboardTypeSupported(type)) { + add_task(async function test_clipboard_asyncGetData_not_support() { + info(`Test asyncGetData request throwing on ${type}`); + SimpleTest.doesThrow( + () => clipboard.asyncGetData(["text/plain"], type, {}), + "Passing unsupported clipboard type should throw" + ); + }); + return; + } + + add_task(async function test_clipboard_asyncGetData_throw() { + info(`Test asyncGetData request throwing on ${type}`); + SimpleTest.doesThrow( + () => clipboard.asyncGetData([], type, {}), + "Passing empty flavor list should throw" + ); + + SimpleTest.doesThrow( + () => clipboard.asyncGetData(["text/plain"], type, null), + "Passing no callback should throw" + ); + }); + + add_task(async function test_clipboard_asyncGetData_no_matched_flavor() { + info(`Test asyncGetData have no matched flavor on ${type}`); + cleanupAllClipboard(); + is(getClipboardData("text/plain", type), null, "ensure clipboard is empty"); + + writeRandomStringToClipboard("text/plain", type); + let request = await new Promise(resolve => { + clipboard.asyncGetData(["text/html"], type, { + QueryInterface: SpecialPowers.ChromeUtils.generateQI([ + "nsIAsyncClipboardGetCallback", + ]), + // nsIAsyncClipboardGetCallback + onSuccess: SpecialPowers.wrapCallback(function ( + aAsyncGetClipboardData + ) { + resolve(aAsyncGetClipboardData); + }), + }); + }); + isDeeply(request.flavorList, [], "Check flavorList"); + }); + + add_task(async function test_empty_data() { + info(`Test asyncGetData request with empty data on ${type}`); + cleanupAllClipboard(); + is(getClipboardData("text/plain", type), null, "ensure clipboard is empty"); + + let request = await asyncGetClipboardData(type); + isDeeply(request.flavorList, [], "Check flavorList"); + await asyncClipboardRequestGetData(request, "text/plain", true).catch( + () => {} + ); + }); + + add_task(async function test_clipboard_asyncGetData_after_write() { + info(`Test asyncGetData request after write on ${type}`); + + let str = writeRandomStringToClipboard("text/plain", type); + let request = await asyncGetClipboardData(type); + isDeeply(request.flavorList, ["text/plain"], "Check flavorList"); + is( + await asyncClipboardRequestGetData(request, "text/plain"), + str, + "Check data" + ); + ok(request.valid, "request should still be valid"); + // Requesting a flavor that is not in the list should throw error. + await asyncClipboardRequestGetData(request, "text/html", true).catch( + () => {} + ); + ok(request.valid, "request should still be valid"); + + // Writing a new data should invalid existing get request. + str = writeRandomStringToClipboard("text/plain", type); + await asyncClipboardRequestGetData(request, "text/plain").then( + () => { + ok(false, "asyncClipboardRequestGetData should not success"); + }, + e => { + ok(true, "asyncClipboardRequestGetData should reject"); + } + ); + ok(!request.valid, "request should no longer be valid"); + + info(`check clipboard data again`); + request = await asyncGetClipboardData(type); + isDeeply(request.flavorList, ["text/plain"], "Check flavorList"); + is( + await asyncClipboardRequestGetData(request, "text/plain"), + str, + "Check data" + ); + + cleanupAllClipboard(); + }); + + add_task(async function test_clipboard_asyncGetData_after_empty() { + info(`Test asyncGetData request after empty on ${type}`); + + let str = writeRandomStringToClipboard("text/plain", type); + let request = await asyncGetClipboardData(type); + isDeeply(request.flavorList, ["text/plain"], "Check flavorList"); + is( + await asyncClipboardRequestGetData(request, "text/plain"), + str, + "Check data" + ); + ok(request.valid, "request should still be valid"); + + // Empty clipboard data + emptyClipboardData(type); + is(getClipboardData("text/plain", type), null, "ensure clipboard is empty"); + + await asyncClipboardRequestGetData(request, "text/plain").then( + () => { + ok(false, "asyncClipboardRequestGetData should not success"); + }, + e => { + ok(true, "asyncClipboardRequestGetData should reject"); + } + ); + ok(!request.valid, "request should no longer be valid"); + + info(`check clipboard data again`); + request = await asyncGetClipboardData(type); + isDeeply(request.flavorList, [], "Check flavorList"); + + cleanupAllClipboard(); + }); +}); + +add_task(async function test_html_data() { + info(`Test asyncGetData request with html data`); + + const html_str = ``; + writeStringToClipboard(html_str, "text/html", clipboard.kGlobalClipboard); + + let request = await asyncGetClipboardData(clipboard.kGlobalClipboard); + isDeeply(request.flavorList, ["text/html"], "Check flavorList"); + is( + await asyncClipboardRequestGetData(request, "text/html"), + // On Windows, widget adds extra data into HTML clipboard. + navigator.platform.includes("Win") + ? `\n${html_str}\n\n` + : html_str, + "Check data" + ); + // Requesting a flavor that is not in the list should throw error. + await asyncClipboardRequestGetData(request, "text/plain", true).catch( + () => {} + ); +}); diff --git a/widget/tests/mochitest.toml b/widget/tests/mochitest.toml index c75833ecbfda..c8fdc1c63aab 100644 --- a/widget/tests/mochitest.toml +++ b/widget/tests/mochitest.toml @@ -24,6 +24,10 @@ skip-if = [ ] support-files = ["file_test_clipboard.js"] +["test_clipboard_asyncGetData.html"] +skip-if = ["display == 'wayland'"] # Bug 1864211 +support-files = ["file_test_clipboard_asyncGetData.js"] + ["test_clipboard_asyncSetData.html"] support-files = ["file_test_clipboard_asyncSetData.js"] diff --git a/widget/tests/test_clipboard_asyncGetData.html b/widget/tests/test_clipboard_asyncGetData.html new file mode 100644 index 000000000000..0cb3dc2aa16c --- /dev/null +++ b/widget/tests/test_clipboard_asyncGetData.html @@ -0,0 +1,19 @@ + + + + +Test for Bug 1852947 + + + + + +

+ +

+
+
+
+
diff --git a/widget/tests/test_clipboard_asyncGetData_chrome.html b/widget/tests/test_clipboard_asyncGetData_chrome.html
new file mode 100644
index 000000000000..d4e44185a52e
--- /dev/null
+++ b/widget/tests/test_clipboard_asyncGetData_chrome.html
@@ -0,0 +1,19 @@
+
+
+
+
+Test for Bug 1852947
+
+
+
+
+
+

+ +

+
+
+
+
diff --git a/widget/tests/test_clipboard_cache_chrome.html b/widget/tests/test_clipboard_cache_chrome.html
index 5ab94290c1b7..55b6d4158946 100644
--- a/widget/tests/test_clipboard_cache_chrome.html
+++ b/widget/tests/test_clipboard_cache_chrome.html
@@ -168,6 +168,50 @@ function runClipboardCacheTests(aIsSupportGetFromCachedTransferable) {
       cleanupAllClipboard();
     });
 
+    add_task(async function test_clipboard_asyncGetData() {
+      const testClipboardData = async function(aRequest, aExpectedData) {
+        is(aRequest.flavorList.length, Object.keys(aExpectedData).length, "Check flavorList length");
+        for (const [key, value] of Object.entries(aExpectedData)) {
+          ok(aRequest.flavorList.includes(key), `${key} should be available`);
+          is(await asyncClipboardRequestGetData(aRequest, key), value,
+             `Check ${key} data`);
+        }
+      };
+
+      info(`test_clipboard_hasDataMatchingFlavors with pref ` +
+           `${aIsSupportGetFromCachedTransferable ? "enabled" : "disabled"}`);
+
+      const clipboardData = { "text/plain": generateRandomString() };
+      const trans = generateNewTransferable("text/plain", clipboardData["text/plain"]);
+
+      info(`Write text/plain data to clipboard ${type}`);
+      clipboard.setData(trans, null, type);
+      await testClipboardData(await asyncGetClipboardData(type), clipboardData);
+
+      info(`Add text/html data to transferable`);
+      const htmlString = `
${generateRandomString()}
`; + addStringToTransferable("text/html", htmlString, trans); + // XXX macOS uses cached transferable to implement kSelectionCache type, too, + // so it behaves differently than other types. + if (aIsSupportGetFromCachedTransferable || + (type == clipboard.kSelectionCache && !SpecialPowers.isHeadless)) { + clipboardData["text/html"] = htmlString; + } + await testClipboardData(await asyncGetClipboardData(type), clipboardData); + + info(`Should not get the data from other clipboard type`); + clipboardTypes.forEach(async function(otherType) { + if (otherType != type && + clipboard.isClipboardTypeSupported(otherType)) { + info(`Check clipboard type ${otherType}`); + await testClipboardData(await asyncGetClipboardData(otherType), {}); + } + }); + + info(`Check data on clipboard ${type} again`); + await testClipboardData(await asyncGetClipboardData(type), clipboardData); + }); + // Test sync set clipboard data. testClipboardCache(type, false, aIsSupportGetFromCachedTransferable);