/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* 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/dom/ClipboardItem.h" #include "mozilla/dom/Promise.h" #include "mozilla/dom/Record.h" #include "nsComponentManagerUtils.h" #include "nsIClipboard.h" #include "nsIInputStream.h" #include "nsISupportsPrimitives.h" #include "nsNetUtil.h" #include "nsServiceManagerUtils.h" namespace mozilla::dom { 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_END NS_IMPL_CYCLE_COLLECTING_ADDREF(ClipboardItem::ItemEntry) NS_IMPL_CYCLE_COLLECTING_RELEASE(ClipboardItem::ItemEntry) void ClipboardItem::ItemEntry::ResolvedCallback(JSContext* aCx, JS::Handle aValue, ErrorResult& aRv) { MOZ_ASSERT(!mLoadingPromise.Exists()); mIsLoadingData = false; OwningStringOrBlob clipboardData; if (!clipboardData.Init(aCx, aValue)) { JS_ClearPendingException(aCx); RejectPendingPromises(NS_ERROR_DOM_DATA_ERR); return; } MaybeResolvePendingPromises(std::move(clipboardData)); } void ClipboardItem::ItemEntry::RejectedCallback(JSContext* aCx, JS::Handle aValue, ErrorResult& aRv) { MOZ_ASSERT(!mLoadingPromise.Exists()); mIsLoadingData = false; RejectPendingPromises(NS_ERROR_DOM_DATA_ERR); } RefPtr ClipboardItem::ItemEntry::GetData() { // Data is still being loaded, either from the system clipboard or the data // promise provided to the ClipboardItem constructor. if (mIsLoadingData) { MOZ_ASSERT(!mData.IsString() && !mData.IsBlob(), "Data should be uninitialized"); MOZ_ASSERT(mLoadResult.isNothing(), "Should have no load result"); MozPromiseHolder holder; RefPtr promise = holder.Ensure(__func__); mPendingGetDataRequests.AppendElement(std::move(holder)); return promise.forget(); } if (NS_FAILED(mLoadResult.value())) { MOZ_ASSERT(!mData.IsString() && !mData.IsBlob(), "Data should be uninitialized"); // We are not able to load data, so reject the promise directly. return GetDataPromise::CreateAndReject(mLoadResult.value(), __func__); } MOZ_ASSERT(mData.IsString() || mData.IsBlob(), "Data should be initialized"); OwningStringOrBlob data(mData); return GetDataPromise::CreateAndResolve(std::move(data), __func__); } void ClipboardItem::ItemEntry::LoadDataFromSystemClipboard( nsITransferable& aTransferable) { // 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(), "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)) { 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(); 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); } 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(), "Should not be in the process of loading data"); mIsLoadingData = true; aDataPromise.AppendNativeHandler(this); } void ClipboardItem::ItemEntry::ReactGetTypePromise(Promise& aPromise) { // Data is still being loaded, either from the system clipboard or the data // promise provided to the ClipboardItem constructor. if (mIsLoadingData) { MOZ_ASSERT(!mData.IsString() && !mData.IsBlob(), "Data should be uninitialized"); MOZ_ASSERT(mLoadResult.isNothing(), "Should have no load result."); mPendingGetTypeRequests.AppendElement(&aPromise); return; } if (NS_FAILED(mLoadResult.value())) { MOZ_ASSERT(!mData.IsString() && !mData.IsBlob(), "Data should be uninitialized"); aPromise.MaybeRejectWithDataError("The data for type '"_ns + NS_ConvertUTF16toUTF8(mType) + "' was not found"_ns); return; } MaybeResolveGetTypePromise(mData, aPromise); } void ClipboardItem::ItemEntry::MaybeResolveGetTypePromise( const OwningStringOrBlob& aData, Promise& aPromise) { if (aData.IsBlob()) { aPromise.MaybeResolve(aData); return; } // XXX This is for the case that data is from ClipboardItem constructor, // maybe we should also load that into a Blob earlier. But Safari returns // different `Blob` instances for each `getTypes` call if the string is from // ClipboardItem constructor, which is more like our current setup. if (RefPtr blob = Blob::CreateStringBlob( mGlobal, NS_ConvertUTF16toUTF8(aData.GetAsString()), mType)) { aPromise.MaybeResolve(blob); return; } aPromise.MaybeRejectWithDataError("The data for type '"_ns + NS_ConvertUTF16toUTF8(mType) + "' was not found"_ns); } void ClipboardItem::ItemEntry::RejectPendingPromises(nsresult aRv) { MOZ_ASSERT(NS_FAILED(aRv), "Should have a failure code here"); 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(), "Should not be in the process of loading data"); mLoadResult.emplace(aRv); auto promiseHolders = std::move(mPendingGetDataRequests); for (auto& promiseHolder : promiseHolders) { promiseHolder.Reject(aRv, __func__); } auto getTypePromises = std::move(mPendingGetTypeRequests); for (auto& promise : getTypePromises) { promise->MaybeReject(aRv); } } void ClipboardItem::ItemEntry::MaybeResolvePendingPromises( OwningStringOrBlob&& aData) { 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(), "Should not be in the process of loading data"); mLoadResult.emplace(NS_OK); mData = std::move(aData); auto getDataPromiseHolders = std::move(mPendingGetDataRequests); for (auto& promiseHolder : getDataPromiseHolders) { OwningStringOrBlob data(mData); promiseHolder.Resolve(std::move(data), __func__); } auto promises = std::move(mPendingGetTypeRequests); for (auto& promise : promises) { MaybeResolveGetTypePromise(mData, *promise); } } NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(ClipboardItem, mOwner, mItems) ClipboardItem::ClipboardItem(nsISupports* aOwner, const dom::PresentationStyle aPresentationStyle, nsTArray>&& aItems) : mOwner(aOwner), mPresentationStyle(aPresentationStyle), mItems(std::move(aItems)) {} // static already_AddRefed ClipboardItem::Constructor( const GlobalObject& aGlobal, const Record>& aItems, const ClipboardItemOptions& aOptions, ErrorResult& aRv) { if (aItems.Entries().IsEmpty()) { aRv.ThrowTypeError("At least one entry required"); return nullptr; } nsCOMPtr global = do_QueryInterface(aGlobal.GetAsSupports()); MOZ_ASSERT(global); nsTArray> items; for (const auto& entry : aItems.Entries()) { RefPtr item = MakeRefPtr(global, entry.mKey); item->LoadDataFromDataPromise(*entry.mValue); items.AppendElement(std::move(item)); } RefPtr item = MakeRefPtr( global, aOptions.mPresentationStyle, std::move(items)); return item.forget(); } void ClipboardItem::GetTypes(nsTArray& aTypes) const { for (const auto& item : mItems) { aTypes.AppendElement(item->Type()); } } already_AddRefed ClipboardItem::GetType(const nsAString& aType, ErrorResult& aRv) { nsCOMPtr global = do_QueryInterface(GetParentObject()); RefPtr p = Promise::Create(global, aRv); if (aRv.Failed()) { return nullptr; } for (auto& item : mItems) { MOZ_ASSERT(item); const nsAString& type = item->Type(); if (type == aType) { nsCOMPtr global = do_QueryInterface(GetParentObject()); if (NS_WARN_IF(!global)) { p->MaybeReject(NS_ERROR_UNEXPECTED); return p.forget(); } item->ReactGetTypePromise(*p); return p.forget(); } } p->MaybeRejectWithNotFoundError( "The type '"_ns + NS_ConvertUTF16toUTF8(aType) + "' was not found"_ns); return p.forget(); } JSObject* ClipboardItem::WrapObject(JSContext* aCx, JS::Handle aGivenProto) { return mozilla::dom::ClipboardItem_Binding::Wrap(aCx, this, aGivenProto); } } // namespace mozilla::dom