gecko-dev/dom/events/Clipboard.cpp

846 lines
27 KiB
C++

/* -*- 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 "nsGlobalWindowInner.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);
}
// Mandatory data types defined in
// https://w3c.github.io/clipboard-apis/#mandatory-data-types-x. The types
// should be in the same order as kNonPlainTextExternalFormats in
// DataTransfer.
static const nsLiteralCString kMandatoryDataTypes[] = {
nsLiteralCString(kHTMLMime), nsLiteralCString(kTextMime),
nsLiteralCString(kPNGImageMime)};
// static
Span<const nsLiteralCString> Clipboard::MandatoryDataTypes() {
return Span<const nsLiteralCString>(kMandatoryDataTypes);
}
namespace {
/**
* This is a base class for ClipboardGetCallbackForRead and
* ClipboardGetCallbackForReadText.
*/
class ClipboardGetCallback : public nsIClipboardGetDataSnapshotCallback {
public:
explicit ClipboardGetCallback(RefPtr<Promise>&& aPromise)
: mPromise(std::move(aPromise)) {}
// nsIClipboardGetDataSnapshotCallback
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
// nsIClipboardGetDataSnapshotCallback
NS_IMETHOD OnSuccess(
nsIClipboardDataSnapshot* aClipboardDataSnapshot) override {
MOZ_ASSERT(mPromise);
MOZ_ASSERT(aClipboardDataSnapshot);
nsTArray<nsCString> flavorList;
nsresult rv = aClipboardDataSnapshot->GetFlavorList(flavorList);
if (NS_FAILED(rv)) {
return OnError(rv);
}
AutoTArray<RefPtr<ClipboardItem::ItemEntry>, 3> entries;
// We might reuse the request from DataTransfer created for paste event,
// which could contain more types that are not in the mandatory list.
for (const auto& format : kMandatoryDataTypes) {
if (flavorList.Contains(format)) {
auto entry = MakeRefPtr<ClipboardItem::ItemEntry>(
mGlobal, NS_ConvertUTF8toUTF16(format));
entry->LoadDataFromSystemClipboard(aClipboardDataSnapshot);
entries.AppendElement(std::move(entry));
}
}
RefPtr<Promise> p(std::move(mPromise));
if (entries.IsEmpty()) {
p->MaybeResolve(nsTArray<RefPtr<ClipboardItem>>{});
return NS_OK;
}
// 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,
nsIClipboardGetDataSnapshotCallback)
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
// nsIClipboardGetDataSnapshotCallback
NS_IMETHOD OnSuccess(
nsIClipboardDataSnapshot* aClipboardDataSnapshot) override {
MOZ_ASSERT(mPromise);
MOZ_ASSERT(!mTransferable);
MOZ_ASSERT(aClipboardDataSnapshot);
AutoTArray<nsCString, 3> flavors;
nsresult rv = aClipboardDataSnapshot->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 = aClipboardDataSnapshot->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,
nsIClipboardGetDataSnapshotCallback,
nsIAsyncClipboardRequestCallback)
} // namespace
void Clipboard::RequestRead(Promise& aPromise, const ReadRequestType& aType,
nsPIDOMWindowInner& aOwner,
nsIPrincipal& aSubjectPrincipal,
nsIClipboardDataSnapshot& aRequest) {
#ifdef DEBUG
bool isValid = false;
MOZ_ASSERT(NS_SUCCEEDED(aRequest.GetValid(&isValid)) && isValid);
#endif
RefPtr<ClipboardGetCallback> callback;
switch (aType) {
case ReadRequestType::eRead: {
callback =
MakeRefPtr<ClipboardGetCallbackForRead>(aOwner.AsGlobal(), &aPromise);
break;
}
case ReadRequestType::eReadText: {
callback = MakeRefPtr<ClipboardGetCallbackForReadText>(&aPromise);
break;
}
default: {
MOZ_ASSERT_UNREACHABLE("Unknown read type");
return;
}
}
MOZ_ASSERT(callback);
callback->OnSuccess(&aRequest);
}
void Clipboard::RequestRead(Promise* aPromise, ReadRequestType aType,
nsPIDOMWindowInner* aOwner,
nsIPrincipal& aPrincipal) {
RefPtr<Promise> p(aPromise);
nsCOMPtr<nsPIDOMWindowInner> owner(aOwner);
nsresult rv;
nsCOMPtr<nsIClipboard> clipboardService(
do_GetService("@mozilla.org/widget/clipboard;1", &rv));
if (NS_FAILED(rv)) {
p->MaybeReject(NS_ERROR_UNEXPECTED);
return;
}
RefPtr<ClipboardGetCallback> callback;
switch (aType) {
case ReadRequestType::eRead: {
nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(owner);
if (NS_WARN_IF(!global)) {
p->MaybeReject(NS_ERROR_UNEXPECTED);
return;
}
AutoTArray<nsCString, std::size(kMandatoryDataTypes)> types;
types.AppendElements(Span<const nsLiteralCString>(kMandatoryDataTypes));
callback = MakeRefPtr<ClipboardGetCallbackForRead>(global, std::move(p));
rv = clipboardService->GetDataSnapshot(
types, nsIClipboard::kGlobalClipboard, owner->GetWindowContext(),
&aPrincipal, callback);
break;
}
case ReadRequestType::eReadText: {
callback = MakeRefPtr<ClipboardGetCallbackForReadText>(std::move(p));
rv = clipboardService->GetDataSnapshot(
AutoTArray<nsCString, 1>{nsLiteralCString(kTextMime)},
nsIClipboard::kGlobalClipboard, owner->GetWindowContext(),
&aPrincipal, callback);
break;
}
default: {
MOZ_ASSERT_UNREACHABLE("Unknown read type");
break;
}
}
if (NS_FAILED(rv)) {
MOZ_ASSERT(callback);
callback->OnError(rv);
return;
}
}
static bool IsReadTextExposedToContent() {
return StaticPrefs::dom_events_asyncClipboard_readText_DoNotUseDirectly();
}
already_AddRefed<Promise> Clipboard::ReadHelper(nsIPrincipal& aSubjectPrincipal,
ReadRequestType aType,
ErrorResult& aRv) {
// Create a new promise
nsGlobalWindowInner* owner = GetOwnerWindow();
RefPtr<Promise> p = dom::Promise::Create(owner, aRv);
if (aRv.Failed()) {
return nullptr;
}
// If a "paste" clipboard event is actively being processed, we're
// intentionally skipping permission/user-activation checks and giving the
// webpage access to the clipboard.
if (RefPtr<DataTransfer> dataTransfer =
owner->GetCurrentPasteDataTransfer()) {
// If there is valid nsIClipboardDataSnapshot, use it directly.
if (nsCOMPtr<nsIClipboardDataSnapshot> clipboardDataSnapshot =
dataTransfer->GetClipboardDataSnapshot()) {
bool isValid = false;
clipboardDataSnapshot->GetValid(&isValid);
if (isValid) {
RequestRead(*p, aType, *owner, aSubjectPrincipal,
*clipboardDataSnapshot);
return p.forget();
}
}
}
if (IsTestingPrefEnabledOrHasReadPermission(aSubjectPrincipal)) {
MOZ_LOG(GetClipboardLog(), LogLevel::Debug,
("%s: testing pref enabled or has read permission", __FUNCTION__));
} else {
// Testing pref is not enabled and no read permission (for extension), so
// need to check user activation.
WindowContext* windowContext = owner->GetWindowContext();
if (!windowContext) {
MOZ_ASSERT_UNREACHABLE("There should be a WindowContext.");
p->MaybeRejectWithUndefined();
return p.forget();
}
// If no transient user activation, reject the promise and return.
if (!windowContext->HasValidTransientUserGestureActivation()) {
p->MaybeRejectWithNotAllowedError(
"Clipboard read request was blocked due to lack of "
"user activation.");
return p.forget();
}
}
RequestRead(p, aType, owner, aSubjectPrincipal);
return p.forget();
}
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 nsIAsyncClipboardRequestCallback {
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) {}
// nsIAsyncClipboardRequestCallback
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, nsIAsyncClipboardRequestCallback)
} // namespace
already_AddRefed<Promise> Clipboard::Write(
const Sequence<OwningNonNull<ClipboardItem>>& aData,
nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) {
// Create a promise
RefPtr<nsGlobalWindowInner> owner = GetOwnerWindow();
RefPtr<Promise> p = dom::Promise::Create(owner, aRv);
if (aRv.Failed()) {
return nullptr;
}
Document* doc = owner->GetDoc();
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,
owner->GetWindowContext(), 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(ToSupports(owner), eCopy,
/* is external */ true,
/* clipboard type */ Nothing());
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>(
ToSupports(GetOwnerWindow()), PresentationStyle::Unspecified,
std::move(items));
sequence.AppendElement(*item);
return Write(std::move(sequence), aSubjectPrincipal, aRv);
}
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