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/. */
6 
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"
35 
36 static mozilla::LazyLogModule gClipboardLog("Clipboard");
37 
38 namespace mozilla::dom {
39 
Clipboard(nsPIDOMWindowInner * aWindow)40 Clipboard::Clipboard(nsPIDOMWindowInner* aWindow)
41     : DOMEventTargetHelper(aWindow) {}
42 
43 Clipboard::~Clipboard() = default;
44 
ReadHelper(nsIPrincipal & aSubjectPrincipal,ClipboardReadType aClipboardReadType,ErrorResult & aRv)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;
52   }
53 
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();
64   }
65 
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);
70 
71   RefPtr<nsPIDOMWindowInner> owner = GetOwner();
72 
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();
83 
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);
93 
94               // Only allow strings and files.
95               if (!found || item->Kind() == DataTransferItem::KIND_OTHER) {
96                 continue;
97               }
98 
99               nsAutoString type;
100               item->GetType(type);
101 
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;
108                 }
109 
110                 nsAutoString string;
111                 if (NS_WARN_IF(NS_FAILED(data->GetAsAString(string)))) {
112                   continue;
113                 }
114 
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;
123                 }
124 
125                 ClipboardItem::ItemEntry* entry = entries.AppendElement();
126                 entry->mType = type;
127                 entry->mData.SetAsBlob() = file;
128               }
129             }
130 
131             nsTArray<RefPtr<ClipboardItem>> sequence;
132             sequence.AppendElement(MakeRefPtr<ClipboardItem>(
133                 owner, PresentationStyle::Unspecified, std::move(entries)));
134             p->MaybeResolve(sequence);
135             break;
136           }
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;
147         }
148       });
149   // Dispatch the runnable
150   GetParentObject()->Dispatch(TaskCategory::Other, r.forget());
151   return p.forget();
152 }
153 
Read(nsIPrincipal & aSubjectPrincipal,ErrorResult & aRv)154 already_AddRefed<Promise> Clipboard::Read(nsIPrincipal& aSubjectPrincipal,
155                                           ErrorResult& aRv) {
156   return ReadHelper(aSubjectPrincipal, eRead, aRv);
157 }
158 
ReadText(nsIPrincipal & aSubjectPrincipal,ErrorResult & aRv)159 already_AddRefed<Promise> Clipboard::ReadText(nsIPrincipal& aSubjectPrincipal,
160                                               ErrorResult& aRv) {
161   return ReadHelper(aSubjectPrincipal, eReadText, aRv);
162 }
163 
164 namespace {
165 
166 struct NativeEntry {
167   nsString mType;
168   nsCOMPtr<nsIVariant> mData;
169 
NativeEntrymozilla::dom::__anonaaea75280211::NativeEntry170   NativeEntry(const nsAString& aType, nsIVariant* aData)
171       : mType(aType), mData(aData) {}
172 };
173 using NativeEntryPromise = MozPromise<NativeEntry, CopyableErrorResult, false>;
174 
175 class BlobTextHandler final : public PromiseNativeHandler {
176  public:
177   NS_DECL_THREADSAFE_ISUPPORTS
178 
BlobTextHandler(const nsAString & aType)179   explicit BlobTextHandler(const nsAString& aType) : mType(aType) {}
180 
Promise()181   RefPtr<NativeEntryPromise> Promise() { return mHolder.Ensure(__func__); }
182 
Reject()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__);
188   }
189 
ResolvedCallback(JSContext * aCx,JS::Handle<JS::Value> aValue)190   void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override {
191     AssertIsOnMainThread();
192 
193     nsString text;
194     if (!ConvertJSValueToUSVString(aCx, aValue, "ClipboardItem text", text)) {
195       Reject();
196       return;
197     }
198 
199     RefPtr<nsVariantCC> variant = new nsVariantCC();
200     variant->SetAsAString(text);
201 
202     NativeEntry native(mType, variant);
203     mHolder.Resolve(std::move(native), __func__);
204   }
205 
RejectedCallback(JSContext * aCx,JS::Handle<JS::Value> aValue)206   void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override {
207     Reject();
208   }
209 
210  private:
211   ~BlobTextHandler() = default;
212 
213   nsString mType;
214   MozPromiseHolder<NativeEntryPromise> mHolder;
215 };
216 
NS_IMPL_ISUPPORTS0(BlobTextHandler)217 NS_IMPL_ISUPPORTS0(BlobTextHandler)
218 
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__);
226   }
227 
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__);
236   }
237   promise->AppendNativeHandler(handler);
238   return handler->Promise();
239 }
240 
241 class ImageDecodeCallback final : public imgIContainerCallback {
242  public:
243   NS_DECL_ISUPPORTS
244 
ImageDecodeCallback(const nsAString & aType)245   explicit ImageDecodeCallback(const nsAString& aType) : mType(aType) {}
246 
Promise()247   RefPtr<NativeEntryPromise> Promise() { return mHolder.Ensure(__func__); }
248 
OnImageReady(imgIContainer * aImage,nsresult aStatus)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;
258     }
259 
260     RefPtr<nsVariantCC> variant = new nsVariantCC();
261     variant->SetAsISupports(aImage);
262 
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;
268   };
269 
270  private:
271   ~ImageDecodeCallback() = default;
272 
273   nsString mType;
274   MozPromiseHolder<NativeEntryPromise> mHolder;
275 };
276 
NS_IMPL_ISUPPORTS(ImageDecodeCallback,imgIContainerCallback)277 NS_IMPL_ISUPPORTS(ImageDecodeCallback, imgIContainerCallback)
278 
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__);
287   }
288 
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__);
297   }
298 
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();
304 }
305 
SanitizeNativeEntry(const NativeEntry & aEntry)306 Result<NativeEntry, ErrorResult> SanitizeNativeEntry(
307     const NativeEntry& aEntry) {
308   MOZ_ASSERT(aEntry.mType.EqualsLiteral(kHTMLMime));
309 
310   nsAutoString string;
311   aEntry.mData->GetAsAString(string);
312 
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));
320   }
321 
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));
330   }
331 
332   RefPtr<nsVariantCC> variant = new nsVariantCC();
333   variant->SetAsAString(sanitized);
334   return NativeEntry(aEntry.mType, variant);
335 }
336 
337 // Restrict to types allowed by Chrome
338 // SVG is still disabled by default in Chrome.
IsValidType(const nsAString & aType)339 static bool IsValidType(const nsAString& aType) {
340   return aType.EqualsLiteral(kPNGImageMime) || aType.EqualsLiteral(kTextMime) ||
341          aType.EqualsLiteral(kHTMLMime);
342 }
343 
344 using NativeItemPromise = NativeEntryPromise::AllPromiseType;
345 
GetClipboardNativeItem(const ClipboardItem & aItem)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__);
354     }
355 
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__);
368               }
369 
370               auto sanitized = SanitizeNativeEntry(aValue.ResolveValue());
371               if (sanitized.isErr()) {
372                 return NativeEntryPromise::CreateAndReject(
373                     CopyableErrorResult(sanitized.unwrapErr()), __func__);
374               }
375               return NativeEntryPromise::CreateAndResolve(sanitized.unwrap(),
376                                                           __func__);
377             });
378       }
379       promises.AppendElement(promise);
380     }
381   }
382   return NativeEntryPromise::All(GetCurrentSerialEventTarget(), promises);
383 }
384 
385 }  // namespace
386 
Write(const Sequence<OwningNonNull<ClipboardItem>> & aData,nsIPrincipal & aSubjectPrincipal,ErrorResult & aRv)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;
394   }
395 
396   RefPtr<nsPIDOMWindowInner> owner = GetOwner();
397   Document* doc = owner ? owner->GetDoc() : nullptr;
398   if (!doc) {
399     p->MaybeRejectWithUndefined();
400     return p.forget();
401   }
402 
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();
412   }
413 
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();
420   }
421 
422   nsCOMPtr<nsILoadContext> context = doc->GetLoadContext();
423   if (!context) {
424     p->MaybeRejectWithUndefined();
425     return p.forget();
426   }
427 
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();
433   }
434 
435   if (aData.Length() == 0) {
436     // Nothing needs to be written to the clipboard.
437     p->MaybeResolveWithUndefined();
438     return p.forget();
439   }
440 
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);
449 
450         for (const auto& entry : aEntries) {
451           nsresult rv = dataTransfer->SetDataWithPrincipal(
452               entry.mType, entry.mData, 0, principal);
453 
454           if (NS_FAILED(rv)) {
455             p->MaybeRejectWithUndefined();
456             return;
457           }
458         }
459 
460         // Get the transferable
461         RefPtr<nsITransferable> transferable =
462             dataTransfer->GetTransferable(0, context);
463         if (!transferable) {
464           p->MaybeRejectWithUndefined();
465           return;
466         }
467 
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;
476         }
477 
478         p->MaybeResolveWithUndefined();
479       },
480       [p](const CopyableErrorResult& aErrorResult) {
481         p->MaybeReject(CopyableErrorResult(aErrorResult));
482       });
483 
484   return p.forget();
485 }
486 
WriteText(const nsAString & aData,nsIPrincipal & aSubjectPrincipal,ErrorResult & aRv)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;
495 
496   nsTArray<OwningNonNull<ClipboardItem>> sequence;
497   RefPtr<ClipboardItem> item = new ClipboardItem(
498       GetOwner(), PresentationStyle::Unspecified, std::move(items));
499   sequence.AppendElement(*item);
500 
501   return Write(std::move(sequence), aSubjectPrincipal, aRv);
502 }
503 
WrapObject(JSContext * aCx,JS::Handle<JSObject * > aGivenProto)504 JSObject* Clipboard::WrapObject(JSContext* aCx,
505                                 JS::Handle<JSObject*> aGivenProto) {
506   return Clipboard_Binding::Wrap(aCx, this, aGivenProto);
507 }
508 
509 /* static */
GetClipboardLog()510 LogModule* Clipboard::GetClipboardLog() { return gClipboardLog; }
511 
512 /* static */
ReadTextEnabled(JSContext * aCx,JSObject * aGlobal)513 bool Clipboard::ReadTextEnabled(JSContext* aCx, JSObject* aGlobal) {
514   nsIPrincipal* prin = nsContentUtils::SubjectPrincipal(aCx);
515   return IsTestingPrefEnabled() || prin->GetIsAddonOrExpandedAddonPrincipal() ||
516          prin->IsSystemPrincipal();
517 }
518 
519 /* static */
IsTestingPrefEnabled()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;
526 }
527 
528 NS_IMPL_CYCLE_COLLECTION_CLASS(Clipboard)
529 
530 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(Clipboard,
531                                                   DOMEventTargetHelper)
532 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
533 
534 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(Clipboard, DOMEventTargetHelper)
535 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
536 
537 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Clipboard)
538 NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
539 
540 NS_IMPL_ADDREF_INHERITED(Clipboard, DOMEventTargetHelper)
541 NS_IMPL_RELEASE_INHERITED(Clipboard, DOMEventTargetHelper)
542 
543 }  // namespace mozilla::dom
544