/* -*- 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, mData, mPendingGetTypeRequests) ClipboardItem::ItemEntry::ItemEntry(const nsAString& aType, const nsACString& aFormat) : mType(aType), mFormat(aFormat) { // XXX https://bugzilla.mozilla.org/show_bug.cgi?id=1776879. // In most of cases, the mType and mFormat are the same, execpt for plain // text. We expose it as "text/plain" to the web, but we use "text/unicode" // internally to retrieve from system clipboard. MOZ_ASSERT_IF( !mType.Equals(NS_ConvertUTF8toUTF16(mFormat)), mType.EqualsLiteral(kTextMime) && mFormat.EqualsLiteral(kUnicodeMime)); } void ClipboardItem::ItemEntry::SetData(already_AddRefed&& aBlob) { // 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( !mLoadingPromise.Exists(), "Should not be in the process of loading data from clipboard."); mData.SetAsBlob() = std::move(aBlob); } void ClipboardItem::ItemEntry::LoadData(nsIGlobalObject& aGlobal, 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( !mLoadingPromise.Exists(), "Should not be in the process of loading data from clipboard."); nsresult rv; nsCOMPtr clipboard( do_GetService("@mozilla.org/widget/clipboard;1", &rv)); if (NS_FAILED(rv)) { return; } nsCOMPtr trans(&aTransferable); clipboard->AsyncGetData(trans, nsIClipboard::kGlobalClipboard) ->Then( GetMainThreadSerialEventTarget(), __func__, /* resolved */ [self = RefPtr{this}, global = RefPtr{&aGlobal}, trans]() { self->mLoadingPromise.Complete(); nsCOMPtr data; nsresult rv = trans->GetTransferData(self->Format().get(), getter_AddRefs(data)); if (NS_WARN_IF(NS_FAILED(rv))) { self->RejectPendingGetTypePromises(rv); return; } RefPtr blob; if (nsCOMPtr supportsstr = do_QueryInterface(data)) { nsAutoString str; supportsstr->GetData(str); blob = Blob::CreateStringBlob(global, 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->RejectPendingGetTypePromises(rv); return; } blob = Blob::CreateMemoryBlob(global, data, available, self->Type()); } else if (nsCOMPtr supportscstr = do_QueryInterface(data)) { nsAutoCString str; supportscstr->GetData(str); blob = Blob::CreateStringBlob(global, str, self->Type()); } if (!blob) { self->RejectPendingGetTypePromises(NS_ERROR_DOM_DATA_ERR); return; } self->ResolvePendingGetTypePromises(*blob); self->SetData(blob.forget()); }, /* rejected */ [self = RefPtr{this}](nsresult rv) { self->mLoadingPromise.Complete(); self->RejectPendingGetTypePromises(rv); }) ->Track(mLoadingPromise); } void ClipboardItem::ItemEntry::ReactPromise(nsIGlobalObject& aGlobal, Promise& aPromise) { // Still loading data from system clipboard. if (mLoadingPromise.Exists()) { mPendingGetTypeRequests.AppendElement(&aPromise); return; } if (mData.IsBlob()) { aPromise.MaybeResolve(mData); 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 (mData.IsString()) { if (RefPtr blob = Blob::CreateStringBlob( &aGlobal, NS_ConvertUTF16toUTF8(mData.GetAsString()), mType)) { aPromise.MaybeResolve(blob); return; } } aPromise.MaybeRejectWithDataError("The data for type '"_ns + NS_ConvertUTF16toUTF8(mType) + "' was not found"_ns); } void ClipboardItem::ItemEntry::RejectPendingGetTypePromises(nsresult rv) { MOZ_ASSERT(!mLoadingPromise.Exists(), "Should not be in the process of loading data from clipboard."); nsTArray> promises = std::move(mPendingGetTypeRequests); for (auto& promise : promises) { promise->MaybeReject(rv); } } void ClipboardItem::ItemEntry::ResolvePendingGetTypePromises(Blob& aBlob) { MOZ_ASSERT(!mLoadingPromise.Exists(), "Should not be in the process of loading data from clipboard."); nsTArray> promises = std::move(mPendingGetTypeRequests); for (auto& promise : promises) { promise->MaybeResolve(&aBlob); } } 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; } nsTArray> items; for (const auto& entry : aItems.Entries()) { nsAutoCString format = entry.mKey.EqualsLiteral(kTextMime) ? nsAutoCString(kUnicodeMime) : NS_ConvertUTF16toUTF8(entry.mKey); items.AppendElement( MakeRefPtr(entry.mKey, format, entry.mValue)); } RefPtr item = MakeRefPtr( aGlobal.GetAsSupports(), 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->ReactPromise(*global, *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