Bug 1852947 - Redesign the async clipboard read API; r=nika

In bug 1755863, we introduce two async APIs in `nsIClipboard` to make async
clipboard API reads the clipboard data asynchronously. When reading, async
clipboard API first check the available types, and then retrieve the actual data
for that type. This process has a potential race condition: the clipboard content
might change between the time between the time we check the types and when we
retrieve the data. Although we currently fetch the actual data immediately after
checking available types (in line with the spec), this minimizes the chance of
encountering this race condition. However, if we would like to support retrieving
the data only when `getType()` is invoked (bug 1691825), this potential race
condition could become a significant issue.

Furthermore, bug 1777448 aims to have a way to track the clipboard data and suppress
the paste context menu when the clipboard data originates from a same-origin page.
This also requires a better way to track read requests, clipboard content and
invalidate the request when the system's clipboard content is changed.

After some refacting around nsBaseClipboard, all platform now use sequence
number to track clipboard content, so `nsIAsyncGetClipboardData` can be associated
with a sequence number and deemed invalid if the associated sequence number isn't
matched the latest system value.

With these new API, it also becomes possible to write some tests.

Depends on D191409

Differential Revision: https://phabricator.services.mozilla.com/D182108
This commit is contained in:
Edgar Chen 2023-11-14 09:21:24 +00:00
parent dacdf12e6a
commit 14d41cb7f9
25 changed files with 1433 additions and 409 deletions

View File

@ -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<Promise>&& aPromise)
: mPromise(std::move(aPromise)) {}
// nsIAsyncClipboardGetCallback
NS_IMETHOD OnError(nsresult aResult) override final {
MOZ_ASSERT(mPromise);
RefPtr<Promise> 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<Promise> mPromise;
};
class ClipboardGetCallbackForRead final : public ClipboardGetCallback {
public:
explicit ClipboardGetCallbackForRead(nsIGlobalObject* aGlobal,
RefPtr<Promise>&& 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<nsCString> flavorList;
nsresult rv = aAsyncGetClipboardData->GetFlavorList(flavorList);
if (NS_FAILED(rv)) {
return OnError(rv);
}
AutoTArray<RefPtr<ClipboardItem::ItemEntry>, 3> entries;
for (const auto& format : flavorList) {
auto entry = MakeRefPtr<ClipboardItem::ItemEntry>(
mGlobal, NS_ConvertUTF8toUTF16(format));
entry->LoadDataFromSystemClipboard(aAsyncGetClipboardData);
entries.AppendElement(std::move(entry));
}
RefPtr<Promise> p(std::move(mPromise));
// We currently only support one clipboard item.
p->MaybeResolve(
AutoTArray<RefPtr<ClipboardItem>, 1>{MakeRefPtr<ClipboardItem>(
mGlobal, PresentationStyle::Unspecified, std::move(entries))});
return NS_OK;
}
protected:
~ClipboardGetCallbackForRead() = default;
nsCOMPtr<nsIGlobalObject> mGlobal;
};
NS_IMPL_ISUPPORTS(ClipboardGetCallbackForRead, nsIAsyncClipboardGetCallback)
class ClipboardGetCallbackForReadText final
: public ClipboardGetCallback,
public nsIAsyncClipboardRequestCallback {
public:
explicit ClipboardGetCallbackForReadText(RefPtr<Promise>&& 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<nsCString, 3> 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<nsISupports> data;
nsresult rv =
mTransferable->GetTransferData(kTextMime, getter_AddRefs(data));
if (!NS_WARN_IF(NS_FAILED(rv))) {
nsCOMPtr<nsISupportsString> supportsstr = do_QueryInterface(data);
MOZ_ASSERT(supportsstr);
if (supportsstr) {
supportsstr->GetData(str);
}
}
RefPtr<Promise> p(std::move(mPromise));
p->MaybeResolve(str);
return NS_OK;
}
protected:
~ClipboardGetCallbackForReadText() = default;
nsCOMPtr<nsITransferable> mTransferable;
};
NS_IMPL_ISUPPORTS(ClipboardGetCallbackForReadText, nsIAsyncClipboardGetCallback,
nsIAsyncClipboardRequestCallback)
} // namespace
void Clipboard::ReadRequest::Answer() {
RefPtr<Promise> p(std::move(mPromise));
RefPtr<nsPIDOMWindowInner> owner(std::move(mOwner));
@ -90,89 +246,30 @@ void Clipboard::ReadRequest::Answer() {
return;
}
RefPtr<ClipboardGetCallback> callback;
switch (mType) {
case ReadRequestType::eRead: {
clipboardService
->AsyncHasDataMatchingFlavors(
// Mandatory data types defined in
// https://w3c.github.io/clipboard-apis/#mandatory-data-types-x
AutoTArray<nsCString, 3>{nsDependentCString(kHTMLMime),
nsDependentCString(kTextMime),
nsDependentCString(kPNGImageMime)},
nsIClipboard::kGlobalClipboard)
->Then(
GetMainThreadSerialEventTarget(), __func__,
/* resolve */
[owner, p](nsTArray<nsCString> formats) {
nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(owner);
if (NS_WARN_IF(!global)) {
p->MaybeReject(NS_ERROR_UNEXPECTED);
return;
}
AutoTArray<RefPtr<ClipboardItem::ItemEntry>, 3> entries;
for (const auto& format : formats) {
nsCOMPtr<nsITransferable> trans =
do_CreateInstance("@mozilla.org/widget/transferable;1");
if (NS_WARN_IF(!trans)) {
continue;
}
trans->Init(nullptr);
trans->AddDataFlavor(format.get());
RefPtr<ClipboardItem::ItemEntry> entry =
MakeRefPtr<ClipboardItem::ItemEntry>(
global, NS_ConvertUTF8toUTF16(format));
entry->LoadDataFromSystemClipboard(*trans);
entries.AppendElement(std::move(entry));
}
// We currently only support one clipboard item.
AutoTArray<RefPtr<ClipboardItem>, 1> items;
items.AppendElement(MakeRefPtr<ClipboardItem>(
global, PresentationStyle::Unspecified,
std::move(entries)));
p->MaybeResolve(std::move(items));
},
/* reject */
[p](nsresult rv) { p->MaybeReject(rv); });
break;
}
case ReadRequestType::eReadText: {
nsCOMPtr<nsITransferable> trans =
do_CreateInstance("@mozilla.org/widget/transferable;1");
if (NS_WARN_IF(!trans)) {
nsCOMPtr<nsIGlobalObject> 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<nsISupports> data;
nsresult rv =
trans->GetTransferData(kTextMime, getter_AddRefs(data));
nsAutoString str;
if (!NS_WARN_IF(NS_FAILED(rv))) {
nsCOMPtr<nsISupportsString> supportsstr =
do_QueryInterface(data);
MOZ_ASSERT(supportsstr);
if (supportsstr) {
supportsstr->GetData(str);
}
}
p->MaybeResolve(str);
},
/* reject */
[p](nsresult rv) { p->MaybeReject(rv); });
callback = MakeRefPtr<ClipboardGetCallbackForRead>(global, std::move(p));
rv = clipboardService->AsyncGetData(
// Mandatory data types defined in
// https://w3c.github.io/clipboard-apis/#mandatory-data-types-x
AutoTArray<nsCString, 3>{nsDependentCString(kHTMLMime),
nsDependentCString(kTextMime),
nsDependentCString(kPNGImageMime)},
nsIClipboard::kGlobalClipboard, callback);
break;
}
case ReadRequestType::eReadText: {
callback = MakeRefPtr<ClipboardGetCallbackForReadText>(std::move(p));
rv = clipboardService->AsyncGetData(
AutoTArray<nsCString, 1>{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<Promise> Clipboard::WriteText(const nsAString& aData,
void Clipboard::ReadRequest::MaybeRejectWithNotAllowedError(
const nsACString& aMessage) {
mPromise->MaybeRejectWithNotAllowedError(aMessage);
RefPtr<Promise> p(std::move(mPromise));
p->MaybeRejectWithNotAllowedError(aMessage);
}
void Clipboard::OnUserReactedToPasteMenuPopup(const bool aAllowed) {

View File

@ -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<JS::Value> 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<JS::Value> 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<nsITransferable> trans = std::move(mTransferable);
if (NS_FAILED(aResult)) {
RejectPendingPromises(aResult);
return NS_OK;
}
MOZ_ASSERT(trans);
nsCOMPtr<nsISupports> 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> blob;
if (nsCOMPtr<nsISupportsString> supportsstr = do_QueryInterface(data)) {
nsAutoString str;
supportsstr->GetData(str);
blob = Blob::CreateStringBlob(mGlobal, NS_ConvertUTF16toUTF8(str), mType);
} else if (nsCOMPtr<nsIInputStream> 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<nsISupportsCString> 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<nsIClipboard> 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<nsITransferable> 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<nsISupports> 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> blob;
if (nsCOMPtr<nsISupportsString> supportsstr =
do_QueryInterface(data)) {
nsAutoString str;
supportsstr->GetData(str);
blob = Blob::CreateStringBlob(
self->mGlobal, NS_ConvertUTF16toUTF8(str), self->Type());
} else if (nsCOMPtr<nsIInputStream> 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<nsISupportsCString> 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);

View File

@ -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<OwningStringOrBlob, nsresult, /* IsExclusive = */ true>;
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<GetDataPromise> 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<GenericPromise> mLoadingPromise;
nsCOMPtr<nsITransferable> mTransferable;
// Pending promises for data retrieval requests.
nsTArray<MozPromiseHolder<GetDataPromise>> mPendingGetDataRequests;

View File

@ -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<PClipboardReadRequestChild>
ContentChild::AllocPClipboardReadRequestChild(
const nsTArray<nsCString>& aTypes) {
return MakeAndAddRef<ClipboardReadRequestChild>(aTypes);
}
media::PMediaChild* ContentChild::AllocPMediaChild() {
return media::AllocPMediaChild();
}

View File

@ -229,6 +229,9 @@ class ContentChild final : public PContentChild,
PRemotePrintJobChild* AllocPRemotePrintJobChild();
already_AddRefed<PClipboardReadRequestChild> AllocPClipboardReadRequestChild(
const nsTArray<nsCString>& aTypes);
PMediaChild* AllocPMediaChild();
bool DeallocPMediaChild(PMediaChild* aActor);

View File

@ -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<nsCString>&& aTypes, const int32_t& aWhichClipboard,
ClipboardHasTypesAsyncResolver&& aResolver) {
nsresult rv;
nsCOMPtr<nsIClipboard> 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<nsCString> types) { aResolver(types); },
/* reject */
[aResolver](nsresult rv) { aResolver(nsTArray<nsCString>{}); });
return IPC_OK();
}
mozilla::ipc::IPCResult ContentParent::RecvGetExternalClipboardFormats(
const int32_t& aWhichClipboard, const bool& aPlainTextOnly,
nsTArray<nsCString>* 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<nsCString> flavors;
nsresult rv = aAsyncGetClipboardData->GetFlavorList(flavors);
if (NS_FAILED(rv)) {
return OnError(rv);
}
auto requestParent = MakeNotNull<RefPtr<ClipboardReadRequestParent>>(
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<ContentParent> mContentParent;
ContentParent::GetClipboardAsyncResolver mResolver;
};
NS_IMPL_ISUPPORTS(ClipboardGetCallback, nsIAsyncClipboardGetCallback)
} // namespace
mozilla::ipc::IPCResult ContentParent::RecvGetClipboardAsync(
nsTArray<nsCString>&& 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<ClipboardGetCallback>(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<nsITransferable> 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();
}

View File

@ -987,10 +987,6 @@ class ContentParent final : public PContentParent,
const int32_t& aWhichClipboard,
bool* aHasType);
mozilla::ipc::IPCResult RecvClipboardHasTypesAsync(
nsTArray<nsCString>&& aTypes, const int32_t& aWhichClipboard,
ClipboardHasTypesAsyncResolver&& aResolver);
mozilla::ipc::IPCResult RecvGetExternalClipboardFormats(
const int32_t& aWhichClipboard, const bool& aPlainTextOnly,
nsTArray<nsCString>* aTypes);

View File

@ -74,6 +74,11 @@ struct IPCTransferableData
IPCTransferableDataItem[] items;
};
union IPCTransferableDataOrError {
IPCTransferableData;
nsresult;
};
struct IPCTransferable
{
IPCTransferableData data;

View File

@ -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.

View File

@ -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<nsCString>& aFlavorList) {
mFlavorList.AppendElements(aFlavorList);
}
NS_INLINE_DECL_REFCOUNTING(ClipboardReadRequestChild)
const nsTArray<nsCString>& FlavorList() const { return mFlavorList; }
protected:
virtual ~ClipboardReadRequestChild() = default;
private:
nsTArray<nsCString> mFlavorList;
};
} // namespace mozilla
#endif // mozilla_ClipboardReadRequestChild_h

View File

@ -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<void(nsresult)>&& 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<void(nsresult)> mCallback;
};
NS_IMPL_ISUPPORTS(ClipboardGetDataCallback, nsIAsyncClipboardRequestCallback)
static Result<nsCOMPtr<nsITransferable>, nsresult> CreateTransferable(
const nsTArray<nsCString>& aTypes) {
nsresult rv;
nsCOMPtr<nsITransferable> 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<nsCString>& 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<nsITransferable> trans = result.unwrap();
RefPtr<ClipboardGetDataCallback> callback =
MakeRefPtr<ClipboardGetDataCallback>([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

View File

@ -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<nsCString>& aFlavors,
GetDataResolver&& aResolver);
private:
~ClipboardReadRequestParent() = default;
RefPtr<ContentParent> mManager;
nsCOMPtr<nsIAsyncGetClipboardData> mAsyncGetClipboardData;
};
} // namespace mozilla
#endif // mozilla_ClipboardReadRequestParent_h

View File

@ -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

View File

@ -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",
]

View File

@ -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<nsCString> flavors;
if (NS_FAILED(aTransferable->FlavorsTransferableCanImport(flavors))) {
return NS_ERROR_FAILURE;
}
for (const auto& flavor : flavors) {
nsCOMPtr<nsISupports> 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<GenericPromise> nsBaseClipboard::AsyncGetData(
nsITransferable* aTransferable, int32_t aWhichClipboard) {
void nsBaseClipboard::MaybeRetryGetAvailableFlavors(
const nsTArray<nsCString>& 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<AsyncGetClipboardData>(
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<nsCString>& 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<nsCString> transferableFlavors;
if (NS_SUCCEEDED(cachedTransferable->FlavorsTransferableCanExport(
transferableFlavors))) {
nsTArray<nsCString> 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<AsyncGetClipboardData>(
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<GenericPromise::Private> dataPromise =
mozilla::MakeRefPtr<GenericPromise::Private>(__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<nsCString>& aFlavorList,
return NS_OK;
}
RefPtr<DataFlavorsPromise> nsBaseClipboard::AsyncHasDataMatchingFlavors(
const nsTArray<nsCString>& 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<nsCString> 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<DataFlavorsPromise::Private> flavorPromise =
mozilla::MakeRefPtr<DataFlavorsPromise::Private>(__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<nsCString>&& 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<nsCString>& 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<nsCString> 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<nsCString> flavors;
if (NS_FAILED(aTransferable->FlavorsTransferableCanImport(flavors))) {
return NS_ERROR_FAILURE;
}
MOZ_ASSERT(mTransferable);
for (const auto& flavor : flavors) {
nsCOMPtr<nsISupports> 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;
}

View File

@ -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<nsCString>& aFlavorList, int32_t aWhichClipboard,
nsIAsyncClipboardGetCallback* aCallback) override final;
NS_IMETHOD EmptyClipboard(int32_t aWhichClipboard) override final;
NS_IMETHOD HasDataMatchingFlavors(const nsTArray<nsCString>& aFlavorList,
int32_t aWhichClipboard,
bool* aOutResult) override final;
NS_IMETHOD IsClipboardTypeSupported(int32_t aWhichClipboard,
bool* aRetval) override final;
RefPtr<mozilla::GenericPromise> AsyncGetData(
nsITransferable* aTransferable, int32_t aWhichClipboard) override final;
RefPtr<DataFlavorsPromise> AsyncHasDataMatchingFlavors(
const nsTArray<nsCString>& aFlavorList,
int32_t aWhichClipboard) override final;
using GetDataCallback = mozilla::MoveOnlyFunction<void(nsresult)>;
using HasMatchingFlavorsCallback = mozilla::MoveOnlyFunction<void(
@ -113,6 +111,33 @@ class nsBaseClipboard : public nsIClipboard {
nsCOMPtr<nsIAsyncClipboardRequestCallback> mCallback;
};
class AsyncGetClipboardData final : public nsIAsyncGetClipboardData {
public:
AsyncGetClipboardData(int32_t aClipboardType, int32_t aSequenceNumber,
nsTArray<nsCString>&& 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<nsCString> mFlavors;
// Data should be read from cache.
const bool mFromCache;
// This is also used to indicate whether this request is still valid.
RefPtr<nsBaseClipboard> 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<nsITransferable> mTransferable;
@ -143,6 +169,11 @@ class nsBaseClipboard : public nsIClipboard {
int32_t mSequenceNumber = -1;
};
void MaybeRetryGetAvailableFlavors(const nsTArray<nsCString>& 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);

View File

@ -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<ClipboardReadRequestChild> 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<nsCString>& 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<nsCString> 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<nsCString>& 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<AsyncGetClipboardDataProxy>(
static_cast<ClipboardReadRequestChild*>(
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<DataFlavorsPromise> nsClipboardProxy::AsyncHasDataMatchingFlavors(
const nsTArray<nsCString>& aFlavorList, int32_t aWhichClipboard) {
auto promise = MakeRefPtr<DataFlavorsPromise::Private>(__func__);
ContentChild::GetSingleton()
->SendClipboardHasTypesAsync(aFlavorList, aWhichClipboard)
->Then(
GetMainThreadSerialEventTarget(), __func__,
/* resolve */
[promise](nsTArray<nsCString> types) {
promise->Resolve(std::move(types), __func__);
},
/* reject */
[promise](mozilla::ipc::ResponseRejectReason aReason) {
promise->Reject(NS_ERROR_FAILURE, __func__);
});
return promise.forget();
}
RefPtr<GenericPromise> 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<nsCString> flavors;
nsresult rv = aTransferable->FlavorsTransferableCanImport(flavors);
if (NS_FAILED(rv)) {
return GenericPromise::CreateAndReject(rv, __func__);
}
nsCOMPtr<nsITransferable> transferable(aTransferable);
auto promise = MakeRefPtr<GenericPromise::Private>(__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();
}

View File

@ -9,17 +9,8 @@
#include "nsITransferable.idl"
#include "nsIClipboardOwner.idl"
%{C++
#include "mozilla/MozPromise.h"
using DataFlavorsPromise = mozilla::MozPromise<nsTArray<nsCString>, nsresult, true>;
%}
interface nsIArray;
native AsyncGetDataPromise(RefPtr<mozilla::GenericPromise>);
native AsyncDataFlavorsPromise(RefPtr<DataFlavorsPromise>);
[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<ACString> 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<ACString> 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<ACString> aFlavorList, in long aWhichClipboard);
};

View File

@ -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"

View File

@ -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")
? `<html><body>\n<!--StartFragment-->${aStr}<!--EndFragment-->\n</body>\n</html>`
: 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);
}
});
}

View File

@ -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 = `<img src="https://example.com/oops">`;
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")
? `<html><body>\n<!--StartFragment-->${html_str}<!--EndFragment-->\n</body>\n</html>`
: html_str,
"Check data"
);
// Requesting a flavor that is not in the list should throw error.
await asyncClipboardRequestGetData(request, "text/plain", true).catch(
() => {}
);
});

View File

@ -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"]

View File

@ -0,0 +1,19 @@
<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=1852947
-->
<head>
<title>Test for Bug 1852947</title>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<script src="clipboard_helper.js"></script>
</head>
<body>
<p id="display"></p>
<div id="content" style="display: none"></div>
<pre id="test"></pre>
<!-- Tests are in file_clipboard_asyncGetData.js -->
<script src="file_test_clipboard_asyncGetData.js"></script>
</body>
</html>

View File

@ -0,0 +1,19 @@
<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=1852947
-->
<head>
<title>Test for Bug 1852947</title>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script src="clipboard_helper.js"></script>
</head>
<body>
<p id="display"></p>
<div id="content" style="display: none"></div>
<pre id="test"></pre>
<!-- Tests are in file_clipboard_asyncGetData.js -->
<script src="file_test_clipboard_asyncGetData.js"></script>
</body>
</html>

View File

@ -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 = `<div>${generateRandomString()}</div>`;
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);