Bug 1726269: part 1) Repeatedly call `::OleSetClipboard` for the Windows-specific...
[gecko.git] / dom / events / Clipboard.cpp
blob57a089b146af224a539dd360fe1f8192f31de41c
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,
47 ErrorResult& aRv) {
48 // Create a new promise
49 RefPtr<Promise> p = dom::Promise::Create(GetOwnerGlobal(), aRv);
50 if (aRv.Failed()) {
51 return nullptr;
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();
63 return p.forget();
66 // Want isExternal = true in order to use the data transfer object to perform
67 // a read
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) {
79 case eRead: {
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++) {
91 bool found = false;
92 DataTransferItem* item = items->IndexedGetter(i, found);
94 // Only allow strings and files.
95 if (!found || item->Kind() == DataTransferItem::KIND_OTHER) {
96 continue;
99 nsAutoString type;
100 item->GetType(type);
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())) {
107 continue;
110 nsAutoString string;
111 if (NS_WARN_IF(NS_FAILED(data->GetAsAString(string)))) {
112 continue;
115 ClipboardItem::ItemEntry* entry = entries.AppendElement();
116 entry->mType = type;
117 entry->mData.SetAsString() = string;
118 } else {
119 IgnoredErrorResult ignored;
120 RefPtr<File> file = item->GetAsFile(*principal, ignored);
121 if (NS_WARN_IF(!file || ignored.Failed())) {
122 continue;
125 ClipboardItem::ItemEntry* entry = entries.AppendElement();
126 entry->mType = type;
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);
135 break;
137 case eReadText:
138 MOZ_LOG(GetClipboardLog(), LogLevel::Debug,
139 ("Clipboard, ReadHelper, read text case\n"));
140 nsAutoString str;
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);
146 break;
149 // Dispatch the runnable
150 GetParentObject()->Dispatch(TaskCategory::Other, r.forget());
151 return p.forget();
154 already_AddRefed<Promise> Clipboard::Read(nsIPrincipal& aSubjectPrincipal,
155 ErrorResult& aRv) {
156 return ReadHelper(aSubjectPrincipal, eRead, aRv);
159 already_AddRefed<Promise> Clipboard::ReadText(nsIPrincipal& aSubjectPrincipal,
160 ErrorResult& aRv) {
161 return ReadHelper(aSubjectPrincipal, eReadText, aRv);
164 namespace {
166 struct NativeEntry {
167 nsString mType;
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 {
176 public:
177 NS_DECL_THREADSAFE_ISUPPORTS
179 explicit BlobTextHandler(const nsAString& aType) : mType(aType) {}
181 RefPtr<NativeEntryPromise> Promise() { return mHolder.Ensure(__func__); }
183 void Reject() {
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();
193 nsString text;
194 if (!ConvertJSValueToUSVString(aCx, aValue, "ClipboardItem text", text)) {
195 Reject();
196 return;
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 {
207 Reject();
210 private:
211 ~BlobTextHandler() = default;
213 nsString mType;
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 {
242 public:
243 NS_DECL_ISUPPORTS
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.
251 int32_t ignored;
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__);
257 return NS_OK;
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),
265 variant);
266 mHolder.Resolve(std::move(native), __func__);
267 return NS_OK;
270 private:
271 ~ImageDecodeCallback() = default;
273 nsString mType;
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));
310 nsAutoString string;
311 aEntry.mData->GetAsAString(string);
313 nsCOMPtr<nsIParserUtils> parserUtils =
314 do_GetService(NS_PARSERUTILS_CONTRACTID);
315 if (!parserUtils) {
316 ErrorResult rv;
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))) {
326 ErrorResult rv;
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));
358 } else {
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(),
367 __func__);
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(),
376 __func__);
379 promises.AppendElement(promise);
382 return NativeEntryPromise::All(GetCurrentSerialEventTarget(), promises);
385 } // namespace
387 already_AddRefed<Promise> Clipboard::Write(
388 const Sequence<OwningNonNull<ClipboardItem>>& aData,
389 nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) {
390 // Create a promise
391 RefPtr<Promise> p = dom::Promise::Create(GetOwnerGlobal(), aRv);
392 if (aRv.Failed()) {
393 return nullptr;
396 RefPtr<nsPIDOMWindowInner> owner = GetOwner();
397 Document* doc = owner ? owner->GetDoc() : nullptr;
398 if (!doc) {
399 p->MaybeRejectWithUndefined();
400 return p.forget();
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.");
411 return p.forget();
414 // Get the clipboard service
415 nsCOMPtr<nsIClipboard> clipboard(
416 do_GetService("@mozilla.org/widget/clipboard;1"));
417 if (!clipboard) {
418 p->MaybeRejectWithUndefined();
419 return p.forget();
422 nsCOMPtr<nsILoadContext> context = doc->GetLoadContext();
423 if (!context) {
424 p->MaybeRejectWithUndefined();
425 return p.forget();
428 if (aData.Length() > 1) {
429 p->MaybeRejectWithNotAllowedError(
430 "Clipboard write is only supported with one ClipboardItem at the "
431 "moment");
432 return p.forget();
435 if (aData.Length() == 0) {
436 // Nothing needs to be written to the clipboard.
437 p->MaybeResolveWithUndefined();
438 return p.forget();
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);
454 if (NS_FAILED(rv)) {
455 p->MaybeRejectWithUndefined();
456 return;
460 // Get the transferable
461 RefPtr<nsITransferable> transferable =
462 dataTransfer->GetTransferable(0, context);
463 if (!transferable) {
464 p->MaybeRejectWithUndefined();
465 return;
468 // Finally write data to clipboard
469 nsresult rv =
470 clipboard->SetData(transferable,
471 /* owner of the transferable */ nullptr,
472 nsIClipboard::kGlobalClipboard);
473 if (NS_FAILED(rv)) {
474 p->MaybeRejectWithUndefined();
475 return;
478 p->MaybeResolveWithUndefined();
480 [p](const CopyableErrorResult& aErrorResult) {
481 p->MaybeReject(CopyableErrorResult(aErrorResult));
484 return p.forget();
487 already_AddRefed<Promise> Clipboard::WriteText(const nsAString& aData,
488 nsIPrincipal& aSubjectPrincipal,
489 ErrorResult& aRv) {
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);
509 /* static */
510 LogModule* Clipboard::GetClipboardLog() { return gClipboardLog; }
512 /* static */
513 bool Clipboard::ReadTextEnabled(JSContext* aCx, JSObject* aGlobal) {
514 nsIPrincipal* prin = nsContentUtils::SubjectPrincipal(aCx);
515 return IsTestingPrefEnabled() || prin->GetIsAddonOrExpandedAddonPrincipal() ||
516 prin->IsSystemPrincipal();
519 /* static */
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