1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "mozilla/AbstractThread.h"
8 #include "mozilla/BasePrincipal.h"
9 #include "mozilla/Result.h"
10 #include "mozilla/ResultVariant.h"
11 #include "mozilla/dom/BlobBinding.h"
12 #include "mozilla/dom/Clipboard.h"
13 #include "mozilla/dom/ClipboardItem.h"
14 #include "mozilla/dom/ClipboardBinding.h"
15 #include "mozilla/dom/Promise.h"
16 #include "mozilla/dom/PromiseNativeHandler.h"
17 #include "mozilla/dom/DataTransfer.h"
18 #include "mozilla/dom/DataTransferItemList.h"
19 #include "mozilla/dom/DataTransferItem.h"
20 #include "mozilla/dom/Document.h"
21 #include "mozilla/StaticPrefs_dom.h"
22 #include "imgIContainer.h"
23 #include "imgITools.h"
24 #include "nsArrayUtils.h"
25 #include "nsComponentManagerUtils.h"
26 #include "nsContentUtils.h"
27 #include "nsIClipboard.h"
28 #include "nsIInputStream.h"
29 #include "nsIParserUtils.h"
30 #include "nsITransferable.h"
31 #include "nsNetUtil.h"
32 #include "nsServiceManagerUtils.h"
33 #include "nsStringStream.h"
34 #include "nsVariant.h"
36 static mozilla::LazyLogModule
gClipboardLog("Clipboard");
38 namespace mozilla::dom
{
40 Clipboard::Clipboard(nsPIDOMWindowInner
* aWindow
)
41 : DOMEventTargetHelper(aWindow
) {}
43 Clipboard::~Clipboard() = default;
45 already_AddRefed
<Promise
> Clipboard::ReadHelper(
46 nsIPrincipal
& aSubjectPrincipal
, ClipboardReadType aClipboardReadType
,
48 // Create a new promise
49 RefPtr
<Promise
> p
= dom::Promise::Create(GetOwnerGlobal(), aRv
);
54 // We want to disable security check for automated tests that have the pref
55 // dom.events.testing.asyncClipboard set to true
56 if (!IsTestingPrefEnabled() &&
57 !nsContentUtils::PrincipalHasPermission(aSubjectPrincipal
,
58 nsGkAtoms::clipboardRead
)) {
59 MOZ_LOG(GetClipboardLog(), LogLevel::Debug
,
60 ("Clipboard, ReadHelper, "
61 "Don't have permissions for reading\n"));
62 p
->MaybeRejectWithUndefined();
66 // Want isExternal = true in order to use the data transfer object to perform
68 RefPtr
<DataTransfer
> dataTransfer
= new DataTransfer(
69 this, ePaste
, /* is external */ true, nsIClipboard::kGlobalClipboard
);
71 RefPtr
<nsPIDOMWindowInner
> owner
= GetOwner();
73 // Create a new runnable
74 RefPtr
<nsIRunnable
> r
= NS_NewRunnableFunction(
75 "Clipboard::Read", [p
, dataTransfer
, aClipboardReadType
, owner
,
76 principal
= RefPtr
{&aSubjectPrincipal
}]() {
77 IgnoredErrorResult ier
;
78 switch (aClipboardReadType
) {
80 MOZ_LOG(GetClipboardLog(), LogLevel::Debug
,
81 ("Clipboard, ReadHelper, read case\n"));
82 dataTransfer
->FillAllExternalData();
84 // Convert the DataTransferItems to ClipboardItems.
85 // FIXME(bug 1691825): This is only suitable for testing!
86 // A real implementation would only read from the clipboard
87 // in ClipboardItem::getType instead of doing it here.
88 nsTArray
<ClipboardItem::ItemEntry
> entries
;
89 DataTransferItemList
* items
= dataTransfer
->Items();
90 for (size_t i
= 0; i
< items
->Length(); i
++) {
92 DataTransferItem
* item
= items
->IndexedGetter(i
, found
);
94 // Only allow strings and files.
95 if (!found
|| item
->Kind() == DataTransferItem::KIND_OTHER
) {
102 if (item
->Kind() == DataTransferItem::KIND_STRING
) {
103 // We just ignore items that we can't access.
104 IgnoredErrorResult ignored
;
105 nsCOMPtr
<nsIVariant
> data
= item
->Data(principal
, ignored
);
106 if (NS_WARN_IF(!data
|| ignored
.Failed())) {
111 if (NS_WARN_IF(NS_FAILED(data
->GetAsAString(string
)))) {
115 ClipboardItem::ItemEntry
* entry
= entries
.AppendElement();
117 entry
->mData
.SetAsString() = string
;
119 IgnoredErrorResult ignored
;
120 RefPtr
<File
> file
= item
->GetAsFile(*principal
, ignored
);
121 if (NS_WARN_IF(!file
|| ignored
.Failed())) {
125 ClipboardItem::ItemEntry
* entry
= entries
.AppendElement();
127 entry
->mData
.SetAsBlob() = file
;
131 nsTArray
<RefPtr
<ClipboardItem
>> sequence
;
132 sequence
.AppendElement(MakeRefPtr
<ClipboardItem
>(
133 owner
, PresentationStyle::Unspecified
, std::move(entries
)));
134 p
->MaybeResolve(sequence
);
138 MOZ_LOG(GetClipboardLog(), LogLevel::Debug
,
139 ("Clipboard, ReadHelper, read text case\n"));
141 dataTransfer
->GetData(NS_LITERAL_STRING_FROM_CSTRING(kTextMime
),
142 str
, *principal
, ier
);
143 // Either resolve with a string extracted from data transfer item
144 // or resolve with an empty string if nothing was found
145 p
->MaybeResolve(str
);
149 // Dispatch the runnable
150 GetParentObject()->Dispatch(TaskCategory::Other
, r
.forget());
154 already_AddRefed
<Promise
> Clipboard::Read(nsIPrincipal
& aSubjectPrincipal
,
156 return ReadHelper(aSubjectPrincipal
, eRead
, aRv
);
159 already_AddRefed
<Promise
> Clipboard::ReadText(nsIPrincipal
& aSubjectPrincipal
,
161 return ReadHelper(aSubjectPrincipal
, eReadText
, aRv
);
168 nsCOMPtr
<nsIVariant
> mData
;
170 NativeEntry(const nsAString
& aType
, nsIVariant
* aData
)
171 : mType(aType
), mData(aData
) {}
173 using NativeEntryPromise
= MozPromise
<NativeEntry
, CopyableErrorResult
, false>;
175 class BlobTextHandler final
: public PromiseNativeHandler
{
177 NS_DECL_THREADSAFE_ISUPPORTS
179 explicit BlobTextHandler(const nsAString
& aType
) : mType(aType
) {}
181 RefPtr
<NativeEntryPromise
> Promise() { return mHolder
.Ensure(__func__
); }
184 CopyableErrorResult rv
;
185 rv
.ThrowUnknownError("Unable to read blob for '"_ns
+
186 NS_ConvertUTF16toUTF8(mType
) + "' as text."_ns
);
187 mHolder
.Reject(rv
, __func__
);
190 void ResolvedCallback(JSContext
* aCx
, JS::Handle
<JS::Value
> aValue
) override
{
191 AssertIsOnMainThread();
194 if (!ConvertJSValueToUSVString(aCx
, aValue
, "ClipboardItem text", text
)) {
199 RefPtr
<nsVariantCC
> variant
= new nsVariantCC();
200 variant
->SetAsAString(text
);
202 NativeEntry
native(mType
, variant
);
203 mHolder
.Resolve(std::move(native
), __func__
);
206 void RejectedCallback(JSContext
* aCx
, JS::Handle
<JS::Value
> aValue
) override
{
211 ~BlobTextHandler() = default;
214 MozPromiseHolder
<NativeEntryPromise
> mHolder
;
217 NS_IMPL_ISUPPORTS0(BlobTextHandler
)
219 RefPtr
<NativeEntryPromise
> GetStringNativeEntry(
220 const ClipboardItem::ItemEntry
& entry
) {
221 if (entry
.mData
.IsString()) {
222 RefPtr
<nsVariantCC
> variant
= new nsVariantCC();
223 variant
->SetAsAString(entry
.mData
.GetAsString());
224 NativeEntry
native(entry
.mType
, variant
);
225 return NativeEntryPromise::CreateAndResolve(native
, __func__
);
228 RefPtr
<BlobTextHandler
> handler
= new BlobTextHandler(entry
.mType
);
229 IgnoredErrorResult ignored
;
230 RefPtr
<Promise
> promise
= entry
.mData
.GetAsBlob()->Text(ignored
);
231 if (ignored
.Failed()) {
232 CopyableErrorResult rv
;
233 rv
.ThrowUnknownError("Unable to read blob for '"_ns
+
234 NS_ConvertUTF16toUTF8(entry
.mType
) + "' as text."_ns
);
235 return NativeEntryPromise::CreateAndReject(rv
, __func__
);
237 promise
->AppendNativeHandler(handler
);
238 return handler
->Promise();
241 class ImageDecodeCallback final
: public imgIContainerCallback
{
245 explicit ImageDecodeCallback(const nsAString
& aType
) : mType(aType
) {}
247 RefPtr
<NativeEntryPromise
> Promise() { return mHolder
.Ensure(__func__
); }
249 NS_IMETHOD
OnImageReady(imgIContainer
* aImage
, nsresult aStatus
) override
{
250 // Request the image's width to force decoding the image header.
252 if (NS_FAILED(aStatus
) || NS_FAILED(aImage
->GetWidth(&ignored
))) {
253 CopyableErrorResult rv
;
254 rv
.ThrowDataError("Unable to decode blob for '"_ns
+
255 NS_ConvertUTF16toUTF8(mType
) + "' as image."_ns
);
256 mHolder
.Reject(rv
, __func__
);
260 RefPtr
<nsVariantCC
> variant
= new nsVariantCC();
261 variant
->SetAsISupports(aImage
);
263 // Note: We always put the image as "native" on the clipboard.
264 NativeEntry
native(NS_LITERAL_STRING_FROM_CSTRING(kNativeImageMime
),
266 mHolder
.Resolve(std::move(native
), __func__
);
271 ~ImageDecodeCallback() = default;
274 MozPromiseHolder
<NativeEntryPromise
> mHolder
;
277 NS_IMPL_ISUPPORTS(ImageDecodeCallback
, imgIContainerCallback
)
279 RefPtr
<NativeEntryPromise
> GetImageNativeEntry(
280 const ClipboardItem::ItemEntry
& entry
) {
281 if (entry
.mData
.IsString()) {
282 CopyableErrorResult rv
;
283 rv
.ThrowTypeError("DOMString not supported for '"_ns
+
284 NS_ConvertUTF16toUTF8(entry
.mType
) +
285 "' as image data."_ns
);
286 return NativeEntryPromise::CreateAndReject(rv
, __func__
);
289 IgnoredErrorResult ignored
;
290 nsCOMPtr
<nsIInputStream
> stream
;
291 entry
.mData
.GetAsBlob()->CreateInputStream(getter_AddRefs(stream
), ignored
);
292 if (ignored
.Failed()) {
293 CopyableErrorResult rv
;
294 rv
.ThrowUnknownError("Unable to read blob for '"_ns
+
295 NS_ConvertUTF16toUTF8(entry
.mType
) + "' as image."_ns
);
296 return NativeEntryPromise::CreateAndReject(rv
, __func__
);
299 RefPtr
<ImageDecodeCallback
> callback
= new ImageDecodeCallback(entry
.mType
);
300 nsCOMPtr
<imgITools
> imgtool
= do_CreateInstance("@mozilla.org/image/tools;1");
301 imgtool
->DecodeImageAsync(stream
, NS_ConvertUTF16toUTF8(entry
.mType
),
302 callback
, GetMainThreadSerialEventTarget());
303 return callback
->Promise();
306 Result
<NativeEntry
, ErrorResult
> SanitizeNativeEntry(
307 const NativeEntry
& aEntry
) {
308 MOZ_ASSERT(aEntry
.mType
.EqualsLiteral(kHTMLMime
));
311 aEntry
.mData
->GetAsAString(string
);
313 nsCOMPtr
<nsIParserUtils
> parserUtils
=
314 do_GetService(NS_PARSERUTILS_CONTRACTID
);
317 rv
.ThrowUnknownError("Error while processing '"_ns
+
318 NS_ConvertUTF16toUTF8(aEntry
.mType
) + "'."_ns
);
319 return Err(std::move(rv
));
322 uint32_t flags
= nsIParserUtils::SanitizerAllowStyle
|
323 nsIParserUtils::SanitizerAllowComments
;
324 nsAutoString sanitized
;
325 if (NS_FAILED(parserUtils
->Sanitize(string
, flags
, sanitized
))) {
327 rv
.ThrowUnknownError("Error while processing '"_ns
+
328 NS_ConvertUTF16toUTF8(aEntry
.mType
) + "'."_ns
);
329 return Err(std::move(rv
));
332 RefPtr
<nsVariantCC
> variant
= new nsVariantCC();
333 variant
->SetAsAString(sanitized
);
334 return NativeEntry(aEntry
.mType
, variant
);
337 // Restrict to types allowed by Chrome
338 // SVG is still disabled by default in Chrome.
339 static bool IsValidType(const nsAString
& aType
) {
340 return aType
.EqualsLiteral(kPNGImageMime
) || aType
.EqualsLiteral(kTextMime
) ||
341 aType
.EqualsLiteral(kHTMLMime
);
344 using NativeItemPromise
= NativeEntryPromise::AllPromiseType
;
346 RefPtr
<NativeItemPromise
> GetClipboardNativeItem(const ClipboardItem
& aItem
) {
347 nsTArray
<RefPtr
<NativeEntryPromise
>> promises
;
348 for (const auto& entry
: aItem
.Entries()) {
349 if (!IsValidType(entry
.mType
)) {
350 CopyableErrorResult rv
;
351 rv
.ThrowNotAllowedError("Type '"_ns
+ NS_ConvertUTF16toUTF8(entry
.mType
) +
352 "' not supported for write"_ns
);
353 return NativeItemPromise::CreateAndReject(rv
, __func__
);
356 if (entry
.mType
.EqualsLiteral(kPNGImageMime
)) {
357 promises
.AppendElement(GetImageNativeEntry(entry
));
359 RefPtr
<NativeEntryPromise
> promise
= GetStringNativeEntry(entry
);
360 if (entry
.mType
.EqualsLiteral(kHTMLMime
)) {
361 promise
= promise
->Then(
362 GetMainThreadSerialEventTarget(), __func__
,
363 [](const NativeEntryPromise::ResolveOrRejectValue
& aValue
)
364 -> RefPtr
<NativeEntryPromise
> {
365 if (aValue
.IsReject()) {
366 return NativeEntryPromise::CreateAndReject(aValue
.RejectValue(),
370 auto sanitized
= SanitizeNativeEntry(aValue
.ResolveValue());
371 if (sanitized
.isErr()) {
372 return NativeEntryPromise::CreateAndReject(
373 CopyableErrorResult(sanitized
.unwrapErr()), __func__
);
375 return NativeEntryPromise::CreateAndResolve(sanitized
.unwrap(),
379 promises
.AppendElement(promise
);
382 return NativeEntryPromise::All(GetCurrentSerialEventTarget(), promises
);
387 already_AddRefed
<Promise
> Clipboard::Write(
388 const Sequence
<OwningNonNull
<ClipboardItem
>>& aData
,
389 nsIPrincipal
& aSubjectPrincipal
, ErrorResult
& aRv
) {
391 RefPtr
<Promise
> p
= dom::Promise::Create(GetOwnerGlobal(), aRv
);
396 RefPtr
<nsPIDOMWindowInner
> owner
= GetOwner();
397 Document
* doc
= owner
? owner
->GetDoc() : nullptr;
399 p
->MaybeRejectWithUndefined();
403 // We want to disable security check for automated tests that have the pref
404 // dom.events.testing.asyncClipboard set to true
405 if (!IsTestingPrefEnabled() &&
406 !nsContentUtils::IsCutCopyAllowed(doc
, aSubjectPrincipal
)) {
407 MOZ_LOG(GetClipboardLog(), LogLevel::Debug
,
408 ("Clipboard, Write, Not allowed to write to clipboard\n"));
409 p
->MaybeRejectWithNotAllowedError(
410 "Clipboard write was blocked due to lack of user activation.");
414 // Get the clipboard service
415 nsCOMPtr
<nsIClipboard
> clipboard(
416 do_GetService("@mozilla.org/widget/clipboard;1"));
418 p
->MaybeRejectWithUndefined();
422 nsCOMPtr
<nsILoadContext
> context
= doc
->GetLoadContext();
424 p
->MaybeRejectWithUndefined();
428 if (aData
.Length() > 1) {
429 p
->MaybeRejectWithNotAllowedError(
430 "Clipboard write is only supported with one ClipboardItem at the "
435 if (aData
.Length() == 0) {
436 // Nothing needs to be written to the clipboard.
437 p
->MaybeResolveWithUndefined();
441 GetClipboardNativeItem(aData
[0])->Then(
442 GetMainThreadSerialEventTarget(), __func__
,
443 [owner
, p
, clipboard
, context
, principal
= RefPtr
{&aSubjectPrincipal
}](
444 const nsTArray
<NativeEntry
>& aEntries
) {
445 RefPtr
<DataTransfer
> dataTransfer
=
446 new DataTransfer(owner
, eCopy
,
447 /* is external */ true,
448 /* clipboard type */ -1);
450 for (const auto& entry
: aEntries
) {
451 nsresult rv
= dataTransfer
->SetDataWithPrincipal(
452 entry
.mType
, entry
.mData
, 0, principal
);
455 p
->MaybeRejectWithUndefined();
460 // Get the transferable
461 RefPtr
<nsITransferable
> transferable
=
462 dataTransfer
->GetTransferable(0, context
);
464 p
->MaybeRejectWithUndefined();
468 // Finally write data to clipboard
470 clipboard
->SetData(transferable
,
471 /* owner of the transferable */ nullptr,
472 nsIClipboard::kGlobalClipboard
);
474 p
->MaybeRejectWithUndefined();
478 p
->MaybeResolveWithUndefined();
480 [p
](const CopyableErrorResult
& aErrorResult
) {
481 p
->MaybeReject(CopyableErrorResult(aErrorResult
));
487 already_AddRefed
<Promise
> Clipboard::WriteText(const nsAString
& aData
,
488 nsIPrincipal
& aSubjectPrincipal
,
490 // Create a single-element Sequence to reuse Clipboard::Write.
491 nsTArray
<ClipboardItem::ItemEntry
> items
;
492 ClipboardItem::ItemEntry
* entry
= items
.AppendElement();
493 entry
->mType
= NS_LITERAL_STRING_FROM_CSTRING(kTextMime
);
494 entry
->mData
.SetAsString() = aData
;
496 nsTArray
<OwningNonNull
<ClipboardItem
>> sequence
;
497 RefPtr
<ClipboardItem
> item
= new ClipboardItem(
498 GetOwner(), PresentationStyle::Unspecified
, std::move(items
));
499 sequence
.AppendElement(*item
);
501 return Write(std::move(sequence
), aSubjectPrincipal
, aRv
);
504 JSObject
* Clipboard::WrapObject(JSContext
* aCx
,
505 JS::Handle
<JSObject
*> aGivenProto
) {
506 return Clipboard_Binding::Wrap(aCx
, this, aGivenProto
);
510 LogModule
* Clipboard::GetClipboardLog() { return gClipboardLog
; }
513 bool Clipboard::ReadTextEnabled(JSContext
* aCx
, JSObject
* aGlobal
) {
514 nsIPrincipal
* prin
= nsContentUtils::SubjectPrincipal(aCx
);
515 return IsTestingPrefEnabled() || prin
->GetIsAddonOrExpandedAddonPrincipal() ||
516 prin
->IsSystemPrincipal();
520 bool Clipboard::IsTestingPrefEnabled() {
521 bool clipboardTestingEnabled
=
522 StaticPrefs::dom_events_testing_asyncClipboard_DoNotUseDirectly();
523 MOZ_LOG(GetClipboardLog(), LogLevel::Debug
,
524 ("Clipboard, Is testing enabled? %d\n", clipboardTestingEnabled
));
525 return clipboardTestingEnabled
;
528 NS_IMPL_CYCLE_COLLECTION_CLASS(Clipboard
)
530 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(Clipboard
,
531 DOMEventTargetHelper
)
532 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
534 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(Clipboard
, DOMEventTargetHelper
)
535 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
537 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Clipboard
)
538 NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper
)
540 NS_IMPL_ADDREF_INHERITED(Clipboard
, DOMEventTargetHelper
)
541 NS_IMPL_RELEASE_INHERITED(Clipboard
, DOMEventTargetHelper
)
543 } // namespace mozilla::dom