diff options
Diffstat (limited to 'dom/events/Clipboard.cpp')
-rw-r--r-- | dom/events/Clipboard.cpp | 820 |
1 files changed, 820 insertions, 0 deletions
diff --git a/dom/events/Clipboard.cpp b/dom/events/Clipboard.cpp new file mode 100644 index 0000000000..090f06834b --- /dev/null +++ b/dom/events/Clipboard.cpp @@ -0,0 +1,820 @@ +/* -*- 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/Clipboard.h" + +#include <algorithm> + +#include "mozilla/AbstractThread.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/RefPtr.h" +#include "mozilla/Result.h" +#include "mozilla/ResultVariant.h" +#include "mozilla/dom/BlobBinding.h" +#include "mozilla/dom/ClipboardItem.h" +#include "mozilla/dom/ClipboardBinding.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/PromiseNativeHandler.h" +#include "mozilla/dom/DataTransfer.h" +#include "mozilla/dom/DataTransferItemList.h" +#include "mozilla/dom/DataTransferItem.h" +#include "mozilla/dom/Document.h" +#include "mozilla/StaticPrefs_dom.h" +#include "imgIContainer.h" +#include "imgITools.h" +#include "nsArrayUtils.h" +#include "nsComponentManagerUtils.h" +#include "nsContentUtils.h" +#include "nsIClipboard.h" +#include "nsIInputStream.h" +#include "nsIParserUtils.h" +#include "nsISupportsPrimitives.h" +#include "nsITransferable.h" +#include "nsNetUtil.h" +#include "nsServiceManagerUtils.h" +#include "nsStringStream.h" +#include "nsTArray.h" +#include "nsThreadUtils.h" +#include "nsVariant.h" + +static mozilla::LazyLogModule gClipboardLog("Clipboard"); + +namespace mozilla::dom { + +Clipboard::Clipboard(nsPIDOMWindowInner* aWindow) + : DOMEventTargetHelper(aWindow) {} + +Clipboard::~Clipboard() = default; + +// static +bool Clipboard::IsTestingPrefEnabledOrHasReadPermission( + nsIPrincipal& aSubjectPrincipal) { + return IsTestingPrefEnabled() || + nsContentUtils::PrincipalHasPermission(aSubjectPrincipal, + nsGkAtoms::clipboardRead); +} + +// @return true iff the event was dispatched successfully. +static bool MaybeCreateAndDispatchMozClipboardReadPasteEvent( + nsPIDOMWindowInner& aOwner) { + RefPtr<Document> document = aOwner.GetDoc(); + + if (!document) { + // Presumably, this shouldn't happen but to be safe, this case is handled. + MOZ_LOG(Clipboard::GetClipboardLog(), LogLevel::Debug, + ("%s: no document.", __FUNCTION__)); + return false; + } + + // Conceptionally, `ClipboardReadPasteChild` is the target of the event. + // It ensures to receive the event by declaring the event in + // <BrowserGlue.sys.mjs>. + return !NS_WARN_IF(NS_FAILED(nsContentUtils::DispatchChromeEvent( + document, ToSupports(document), u"MozClipboardReadPaste"_ns, + CanBubble::eNo, Cancelable::eNo))); +} + +void Clipboard::ReadRequest::Answer() { + RefPtr<Promise> p(std::move(mPromise)); + RefPtr<nsPIDOMWindowInner> owner(std::move(mOwner)); + + nsresult rv; + nsCOMPtr<nsIClipboard> clipboardService( + do_GetService("@mozilla.org/widget/clipboard;1", &rv)); + if (NS_FAILED(rv)) { + p->MaybeReject(NS_ERROR_UNEXPECTED); + return; + } + + 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)) { + 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); }); + break; + } + default: { + MOZ_ASSERT_UNREACHABLE("Unknown read type"); + break; + } + } +} + +static bool IsReadTextExposedToContent() { + return StaticPrefs::dom_events_asyncClipboard_readText_DoNotUseDirectly(); +} + +void Clipboard::CheckReadPermissionAndHandleRequest( + Promise& aPromise, nsIPrincipal& aSubjectPrincipal, ReadRequestType aType) { + if (IsTestingPrefEnabledOrHasReadPermission(aSubjectPrincipal)) { + MOZ_LOG(GetClipboardLog(), LogLevel::Debug, + ("%s: testing pref enabled or has read permission", __FUNCTION__)); + nsPIDOMWindowInner* owner = GetOwner(); + if (!owner) { + aPromise.MaybeRejectWithUndefined(); + return; + } + + ReadRequest{aPromise, aType, *owner}.Answer(); + return; + } + + if (aSubjectPrincipal.GetIsAddonOrExpandedAddonPrincipal()) { + // TODO: enable showing the "Paste" button in this case; see bug 1773681. + MOZ_LOG(GetClipboardLog(), LogLevel::Debug, + ("%s: Addon without read permssion.", __FUNCTION__)); + aPromise.MaybeRejectWithUndefined(); + return; + } + + HandleReadRequestWhichRequiresPasteButton(aPromise, aType); +} + +void Clipboard::HandleReadRequestWhichRequiresPasteButton( + Promise& aPromise, ReadRequestType aType) { + nsPIDOMWindowInner* owner = GetOwner(); + WindowContext* windowContext = owner ? owner->GetWindowContext() : nullptr; + if (!windowContext) { + MOZ_ASSERT_UNREACHABLE("There should be a WindowContext."); + aPromise.MaybeRejectWithUndefined(); + return; + } + + // If no transient user activation, reject the promise and return. + if (!windowContext->HasValidTransientUserGestureActivation()) { + aPromise.MaybeRejectWithNotAllowedError( + "Clipboard read request was blocked due to lack of " + "user activation."); + return; + } + + // TODO: when a user activation stems from a contextmenu event + // (https://developer.mozilla.org/en-US/docs/Web/API/Element/contextmenu_event), + // forbid pasting (bug 1767941). + + switch (mTransientUserPasteState.RefreshAndGet(*windowContext)) { + case TransientUserPasteState::Value::Initial: { + MOZ_ASSERT(mReadRequests.IsEmpty()); + + if (MaybeCreateAndDispatchMozClipboardReadPasteEvent(*owner)) { + mTransientUserPasteState.OnStartWaitingForUserReactionToPasteMenuPopup( + windowContext->GetUserGestureStart()); + mReadRequests.AppendElement( + MakeUnique<ReadRequest>(aPromise, aType, *owner)); + } else { + // This shouldn't happen but let's handle this case. + aPromise.MaybeRejectWithUndefined(); + } + break; + } + case TransientUserPasteState::Value:: + WaitingForUserReactionToPasteMenuPopup: { + MOZ_ASSERT(!mReadRequests.IsEmpty()); + + mReadRequests.AppendElement( + MakeUnique<ReadRequest>(aPromise, aType, *owner)); + break; + } + case TransientUserPasteState::Value::TransientlyForbiddenByUser: { + aPromise.MaybeRejectWithNotAllowedError( + "`Clipboard read request was blocked due to the user " + "dismissing the 'Paste' button."); + break; + } + case TransientUserPasteState::Value::TransientlyAllowedByUser: { + ReadRequest{aPromise, aType, *owner}.Answer(); + break; + } + } +} + +already_AddRefed<Promise> Clipboard::ReadHelper(nsIPrincipal& aSubjectPrincipal, + ReadRequestType aType, + ErrorResult& aRv) { + // Create a new promise + RefPtr<Promise> p = dom::Promise::Create(GetOwnerGlobal(), aRv); + if (aRv.Failed()) { + return nullptr; + } + + CheckReadPermissionAndHandleRequest(*p, aSubjectPrincipal, aType); + return p.forget(); +} + +auto Clipboard::TransientUserPasteState::RefreshAndGet( + WindowContext& aWindowContext) -> Value { + MOZ_ASSERT(aWindowContext.HasValidTransientUserGestureActivation()); + + switch (mValue) { + case Value::Initial: { + MOZ_ASSERT(mUserGestureStart.IsNull()); + break; + } + case Value::WaitingForUserReactionToPasteMenuPopup: { + MOZ_ASSERT(!mUserGestureStart.IsNull()); + MOZ_ASSERT( + mUserGestureStart == aWindowContext.GetUserGestureStart(), + "A new transient user gesture activation should be impossible while " + "there's no response to the 'Paste' button."); + // `OnUserReactedToPasteMenuPopup` will handle the reaction. + break; + } + case Value::TransientlyForbiddenByUser: { + [[fallthrough]]; + } + case Value::TransientlyAllowedByUser: { + MOZ_ASSERT(!mUserGestureStart.IsNull()); + + if (mUserGestureStart != aWindowContext.GetUserGestureStart()) { + *this = {}; + } + break; + } + } + + return mValue; +} + +void Clipboard::TransientUserPasteState:: + OnStartWaitingForUserReactionToPasteMenuPopup( + const TimeStamp& aUserGestureStart) { + MOZ_ASSERT(mValue == Value::Initial); + MOZ_ASSERT(!aUserGestureStart.IsNull()); + + mValue = Value::WaitingForUserReactionToPasteMenuPopup; + mUserGestureStart = aUserGestureStart; +} + +void Clipboard::TransientUserPasteState::OnUserReactedToPasteMenuPopup( + const bool aAllowed) { + mValue = aAllowed ? Value::TransientlyAllowedByUser + : Value::TransientlyForbiddenByUser; +} + +already_AddRefed<Promise> Clipboard::Read(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) { + return ReadHelper(aSubjectPrincipal, ReadRequestType::eRead, aRv); +} + +already_AddRefed<Promise> Clipboard::ReadText(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) { + return ReadHelper(aSubjectPrincipal, ReadRequestType::eReadText, aRv); +} + +namespace { + +struct NativeEntry { + nsString mType; + nsCOMPtr<nsIVariant> mData; + + NativeEntry(const nsAString& aType, nsIVariant* aData) + : mType(aType), mData(aData) {} +}; +using NativeEntryPromise = MozPromise<NativeEntry, CopyableErrorResult, false>; + +class BlobTextHandler final : public PromiseNativeHandler { + public: + NS_DECL_THREADSAFE_ISUPPORTS + + explicit BlobTextHandler(const nsAString& aType) : mType(aType) {} + + RefPtr<NativeEntryPromise> Promise() { return mHolder.Ensure(__func__); } + + void Reject() { + CopyableErrorResult rv; + rv.ThrowUnknownError("Unable to read blob for '"_ns + + NS_ConvertUTF16toUTF8(mType) + "' as text."_ns); + mHolder.Reject(rv, __func__); + } + + void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override { + AssertIsOnMainThread(); + + nsString text; + if (!ConvertJSValueToUSVString(aCx, aValue, "ClipboardItem text", text)) { + Reject(); + return; + } + + RefPtr<nsVariantCC> variant = new nsVariantCC(); + variant->SetAsAString(text); + + NativeEntry native(mType, variant); + mHolder.Resolve(std::move(native), __func__); + } + + void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override { + Reject(); + } + + private: + ~BlobTextHandler() = default; + + nsString mType; + MozPromiseHolder<NativeEntryPromise> mHolder; +}; + +NS_IMPL_ISUPPORTS0(BlobTextHandler) + +static RefPtr<NativeEntryPromise> GetStringNativeEntry( + const nsAString& aType, const OwningStringOrBlob& aData) { + if (aData.IsString()) { + RefPtr<nsVariantCC> variant = new nsVariantCC(); + variant->SetAsAString(aData.GetAsString()); + NativeEntry native(aType, variant); + return NativeEntryPromise::CreateAndResolve(native, __func__); + } + + RefPtr<BlobTextHandler> handler = new BlobTextHandler(aType); + IgnoredErrorResult ignored; + RefPtr<Promise> promise = aData.GetAsBlob()->Text(ignored); + if (ignored.Failed()) { + CopyableErrorResult rv; + rv.ThrowUnknownError("Unable to read blob for '"_ns + + NS_ConvertUTF16toUTF8(aType) + "' as text."_ns); + return NativeEntryPromise::CreateAndReject(rv, __func__); + } + promise->AppendNativeHandler(handler); + return handler->Promise(); +} + +class ImageDecodeCallback final : public imgIContainerCallback { + public: + NS_DECL_ISUPPORTS + + explicit ImageDecodeCallback(const nsAString& aType) : mType(aType) {} + + RefPtr<NativeEntryPromise> Promise() { return mHolder.Ensure(__func__); } + + NS_IMETHOD OnImageReady(imgIContainer* aImage, nsresult aStatus) override { + // Request the image's width to force decoding the image header. + int32_t ignored; + if (NS_FAILED(aStatus) || NS_FAILED(aImage->GetWidth(&ignored))) { + CopyableErrorResult rv; + rv.ThrowDataError("Unable to decode blob for '"_ns + + NS_ConvertUTF16toUTF8(mType) + "' as image."_ns); + mHolder.Reject(rv, __func__); + return NS_OK; + } + + RefPtr<nsVariantCC> variant = new nsVariantCC(); + variant->SetAsISupports(aImage); + + // Note: We always put the image as "native" on the clipboard. + NativeEntry native(NS_LITERAL_STRING_FROM_CSTRING(kNativeImageMime), + variant); + mHolder.Resolve(std::move(native), __func__); + return NS_OK; + }; + + private: + ~ImageDecodeCallback() = default; + + nsString mType; + MozPromiseHolder<NativeEntryPromise> mHolder; +}; + +NS_IMPL_ISUPPORTS(ImageDecodeCallback, imgIContainerCallback) + +static RefPtr<NativeEntryPromise> GetImageNativeEntry( + const nsAString& aType, const OwningStringOrBlob& aData) { + if (aData.IsString()) { + CopyableErrorResult rv; + rv.ThrowTypeError("DOMString not supported for '"_ns + + NS_ConvertUTF16toUTF8(aType) + "' as image data."_ns); + return NativeEntryPromise::CreateAndReject(rv, __func__); + } + + IgnoredErrorResult ignored; + nsCOMPtr<nsIInputStream> stream; + aData.GetAsBlob()->CreateInputStream(getter_AddRefs(stream), ignored); + if (ignored.Failed()) { + CopyableErrorResult rv; + rv.ThrowUnknownError("Unable to read blob for '"_ns + + NS_ConvertUTF16toUTF8(aType) + "' as image."_ns); + return NativeEntryPromise::CreateAndReject(rv, __func__); + } + + RefPtr<ImageDecodeCallback> callback = new ImageDecodeCallback(aType); + nsCOMPtr<imgITools> imgtool = do_CreateInstance("@mozilla.org/image/tools;1"); + imgtool->DecodeImageAsync(stream, NS_ConvertUTF16toUTF8(aType), callback, + GetMainThreadSerialEventTarget()); + return callback->Promise(); +} + +static Result<NativeEntry, ErrorResult> SanitizeNativeEntry( + const NativeEntry& aEntry) { + MOZ_ASSERT(aEntry.mType.EqualsLiteral(kHTMLMime)); + + nsAutoString string; + aEntry.mData->GetAsAString(string); + + nsCOMPtr<nsIParserUtils> parserUtils = + do_GetService(NS_PARSERUTILS_CONTRACTID); + if (!parserUtils) { + ErrorResult rv; + rv.ThrowUnknownError("Error while processing '"_ns + + NS_ConvertUTF16toUTF8(aEntry.mType) + "'."_ns); + return Err(std::move(rv)); + } + + uint32_t flags = nsIParserUtils::SanitizerAllowStyle | + nsIParserUtils::SanitizerAllowComments; + nsAutoString sanitized; + if (NS_FAILED(parserUtils->Sanitize(string, flags, sanitized))) { + ErrorResult rv; + rv.ThrowUnknownError("Error while processing '"_ns + + NS_ConvertUTF16toUTF8(aEntry.mType) + "'."_ns); + return Err(std::move(rv)); + } + + RefPtr<nsVariantCC> variant = new nsVariantCC(); + variant->SetAsAString(sanitized); + return NativeEntry(aEntry.mType, variant); +} + +static RefPtr<NativeEntryPromise> GetNativeEntry( + const nsAString& aType, const OwningStringOrBlob& aData) { + if (aType.EqualsLiteral(kPNGImageMime)) { + return GetImageNativeEntry(aType, aData); + } + + RefPtr<NativeEntryPromise> promise = GetStringNativeEntry(aType, aData); + if (aType.EqualsLiteral(kHTMLMime)) { + promise = promise->Then( + GetMainThreadSerialEventTarget(), __func__, + [](const NativeEntryPromise::ResolveOrRejectValue& aValue) + -> RefPtr<NativeEntryPromise> { + if (aValue.IsReject()) { + return NativeEntryPromise::CreateAndReject(aValue.RejectValue(), + __func__); + } + + auto sanitized = SanitizeNativeEntry(aValue.ResolveValue()); + if (sanitized.isErr()) { + return NativeEntryPromise::CreateAndReject( + CopyableErrorResult(sanitized.unwrapErr()), __func__); + } + return NativeEntryPromise::CreateAndResolve(sanitized.unwrap(), + __func__); + }); + } + return promise; +} + +// Restrict to types allowed by Chrome +// SVG is still disabled by default in Chrome. +static bool IsValidType(const nsAString& aType) { + return aType.EqualsLiteral(kPNGImageMime) || aType.EqualsLiteral(kTextMime) || + aType.EqualsLiteral(kHTMLMime); +} + +using NativeItemPromise = NativeEntryPromise::AllPromiseType; +static RefPtr<NativeItemPromise> GetClipboardNativeItem( + const ClipboardItem& aItem) { + nsTArray<RefPtr<NativeEntryPromise>> promises; + for (const auto& entry : aItem.Entries()) { + const nsAString& type = entry->Type(); + if (!IsValidType(type)) { + CopyableErrorResult rv; + rv.ThrowNotAllowedError("Type '"_ns + NS_ConvertUTF16toUTF8(type) + + "' not supported for write"_ns); + return NativeItemPromise::CreateAndReject(rv, __func__); + } + + using GetDataPromise = ClipboardItem::ItemEntry::GetDataPromise; + promises.AppendElement(entry->GetData()->Then( + GetMainThreadSerialEventTarget(), __func__, + [t = nsString(type)](const GetDataPromise::ResolveOrRejectValue& aValue) + -> RefPtr<NativeEntryPromise> { + if (aValue.IsReject()) { + return NativeEntryPromise::CreateAndReject( + CopyableErrorResult(aValue.RejectValue()), __func__); + } + + return GetNativeEntry(t, aValue.ResolveValue()); + })); + } + return NativeEntryPromise::All(GetCurrentSerialEventTarget(), promises); +} + +class ClipboardWriteCallback final : public nsIAsyncSetClipboardDataCallback { + public: + // 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 + + explicit ClipboardWriteCallback(Promise* aPromise, + ClipboardItem* aClipboardItem) + : mPromise(aPromise), mClipboardItem(aClipboardItem) {} + + // nsIAsyncSetClipboardDataCallback + NS_IMETHOD OnComplete(nsresult aResult) override { + MOZ_ASSERT(mPromise); + + RefPtr<Promise> promise = std::move(mPromise); + // XXX We need to check state here is because the promise might be rejected + // before the callback is called, we probably could wrap the promise into a + // structure to make it less confused. + if (promise->State() == Promise::PromiseState::Pending) { + if (NS_FAILED(aResult)) { + promise->MaybeRejectWithNotAllowedError( + "Clipboard write is not allowed."); + return NS_OK; + } + + promise->MaybeResolveWithUndefined(); + } + + return NS_OK; + } + + protected: + ~ClipboardWriteCallback() { + // Callback should be notified. + MOZ_ASSERT(!mPromise); + }; + + // It will be reset to nullptr once callback is notified. + RefPtr<Promise> mPromise; + // Keep ClipboardItem alive until clipboard write is done. + RefPtr<ClipboardItem> mClipboardItem; +}; + +NS_IMPL_ISUPPORTS(ClipboardWriteCallback, nsIAsyncSetClipboardDataCallback) + +} // namespace + +already_AddRefed<Promise> Clipboard::Write( + const Sequence<OwningNonNull<ClipboardItem>>& aData, + nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) { + // Create a promise + RefPtr<Promise> p = dom::Promise::Create(GetOwnerGlobal(), aRv); + if (aRv.Failed()) { + return nullptr; + } + + RefPtr<nsPIDOMWindowInner> owner = GetOwner(); + Document* doc = owner ? owner->GetDoc() : nullptr; + if (!doc) { + p->MaybeRejectWithUndefined(); + return p.forget(); + } + + // We want to disable security check for automated tests that have the pref + // dom.events.testing.asyncClipboard set to true + if (!IsTestingPrefEnabled() && + !nsContentUtils::IsCutCopyAllowed(doc, aSubjectPrincipal)) { + MOZ_LOG(GetClipboardLog(), LogLevel::Debug, + ("Clipboard, Write, Not allowed to write to clipboard\n")); + p->MaybeRejectWithNotAllowedError( + "Clipboard write was blocked due to lack of user activation."); + return p.forget(); + } + + // Get the clipboard service + nsCOMPtr<nsIClipboard> clipboard( + do_GetService("@mozilla.org/widget/clipboard;1")); + if (!clipboard) { + p->MaybeRejectWithUndefined(); + return p.forget(); + } + + nsCOMPtr<nsILoadContext> context = doc->GetLoadContext(); + if (!context) { + p->MaybeRejectWithUndefined(); + return p.forget(); + } + + if (aData.Length() > 1) { + p->MaybeRejectWithNotAllowedError( + "Clipboard write is only supported with one ClipboardItem at the " + "moment"); + return p.forget(); + } + + if (aData.Length() == 0) { + // Nothing needs to be written to the clipboard. + p->MaybeResolveWithUndefined(); + return p.forget(); + } + + nsCOMPtr<nsIAsyncSetClipboardData> request; + RefPtr<ClipboardWriteCallback> callback = + MakeRefPtr<ClipboardWriteCallback>(p, aData[0]); + nsresult rv = clipboard->AsyncSetData(nsIClipboard::kGlobalClipboard, + callback, getter_AddRefs(request)); + if (NS_FAILED(rv)) { + p->MaybeReject(rv); + return p.forget(); + } + + GetClipboardNativeItem(aData[0])->Then( + GetMainThreadSerialEventTarget(), __func__, + [owner, request, context, principal = RefPtr{&aSubjectPrincipal}]( + const nsTArray<NativeEntry>& aEntries) { + RefPtr<DataTransfer> dataTransfer = + new DataTransfer(owner, eCopy, + /* is external */ true, + /* clipboard type */ -1); + + for (const auto& entry : aEntries) { + nsresult rv = dataTransfer->SetDataWithPrincipal( + entry.mType, entry.mData, 0, principal); + + if (NS_FAILED(rv)) { + request->Abort(rv); + return; + } + } + + // Get the transferable + RefPtr<nsITransferable> transferable = + dataTransfer->GetTransferable(0, context); + if (!transferable) { + request->Abort(NS_ERROR_FAILURE); + return; + } + + // Finally write data to clipboard + request->SetData(transferable, /* clipboard owner */ nullptr); + }, + [p, request](const CopyableErrorResult& aErrorResult) { + p->MaybeReject(CopyableErrorResult(aErrorResult)); + request->Abort(NS_ERROR_ABORT); + }); + + return p.forget(); +} + +already_AddRefed<Promise> Clipboard::WriteText(const nsAString& aData, + nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) { + nsCOMPtr<nsIGlobalObject> global = GetOwnerGlobal(); + if (!global) { + aRv.ThrowInvalidStateError("Unable to get global."); + return nullptr; + } + + // Create a single-element Sequence to reuse Clipboard::Write. + nsTArray<RefPtr<ClipboardItem::ItemEntry>> items; + items.AppendElement(MakeRefPtr<ClipboardItem::ItemEntry>( + global, NS_LITERAL_STRING_FROM_CSTRING(kTextMime), aData)); + + nsTArray<OwningNonNull<ClipboardItem>> sequence; + RefPtr<ClipboardItem> item = MakeRefPtr<ClipboardItem>( + GetOwner(), PresentationStyle::Unspecified, std::move(items)); + sequence.AppendElement(*item); + + return Write(std::move(sequence), aSubjectPrincipal, aRv); +} + +void Clipboard::ReadRequest::MaybeRejectWithNotAllowedError( + const nsACString& aMessage) { + mPromise->MaybeRejectWithNotAllowedError(aMessage); +} + +void Clipboard::OnUserReactedToPasteMenuPopup(const bool aAllowed) { + MOZ_LOG(GetClipboardLog(), LogLevel::Debug, ("%s", __FUNCTION__)); + + mTransientUserPasteState.OnUserReactedToPasteMenuPopup(aAllowed); + + MOZ_ASSERT(!mReadRequests.IsEmpty()); + + for (UniquePtr<ReadRequest>& request : mReadRequests) { + if (aAllowed) { + request->Answer(); + } else { + request->MaybeRejectWithNotAllowedError( + "The user dismissed the 'Paste' button."_ns); + } + } + + mReadRequests.Clear(); +} + +JSObject* Clipboard::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return Clipboard_Binding::Wrap(aCx, this, aGivenProto); +} + +/* static */ +LogModule* Clipboard::GetClipboardLog() { return gClipboardLog; } + +/* static */ +bool Clipboard::ReadTextEnabled(JSContext* aCx, JSObject* aGlobal) { + nsIPrincipal* prin = nsContentUtils::SubjectPrincipal(aCx); + return IsReadTextExposedToContent() || + prin->GetIsAddonOrExpandedAddonPrincipal() || + prin->IsSystemPrincipal(); +} + +/* static */ +bool Clipboard::IsTestingPrefEnabled() { + bool clipboardTestingEnabled = + StaticPrefs::dom_events_testing_asyncClipboard_DoNotUseDirectly(); + MOZ_LOG(GetClipboardLog(), LogLevel::Debug, + ("Clipboard, Is testing enabled? %d\n", clipboardTestingEnabled)); + return clipboardTestingEnabled; +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(Clipboard) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(Clipboard, + DOMEventTargetHelper) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(Clipboard, DOMEventTargetHelper) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Clipboard) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +NS_IMPL_ADDREF_INHERITED(Clipboard, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(Clipboard, DOMEventTargetHelper) + +} // namespace mozilla::dom |