Bug 1461465 - Implement async Clipboard APIs, r=nika,r=enndeakin draft
authorAnny Gakhokidze <agakhokidze@mozilla.com>
Thu, 31 May 2018 11:57:57 -0400
changeset 822929 cadde77c5679b124cfcff52740f7da4bfdd76e0f
parent 822740 02c8644c45b1f143263d30d769d86c2d1058812e
child 822930 997361b5456884ee045f9a0651fe3d4e04917336
child 822932 619f00ac7329d2cec5cad9cafcc4c3f71d2aae70
push id117532
push userbmo:agakhokidze@mozilla.com
push dateThu, 26 Jul 2018 12:25:16 +0000
reviewersnika, enndeakin
bugs1461465
milestone63.0a1
Bug 1461465 - Implement async Clipboard APIs, r=nika,r=enndeakin MozReview-Commit-ID: 3vCxbaGZtiv
dom/base/Navigator.cpp
dom/base/Navigator.h
dom/bindings/Bindings.conf
dom/events/Clipboard.cpp
dom/events/Clipboard.h
dom/events/DataTransfer.cpp
dom/events/DataTransfer.h
dom/events/DataTransferItem.h
dom/events/DataTransferItemList.cpp
dom/events/Event.cpp
dom/events/moz.build
dom/webidl/Clipboard.webidl
dom/webidl/Navigator.webidl
dom/webidl/moz.build
modules/libpref/init/all.js
testing/web-platform/meta/clipboard-apis/__dir__.ini
testing/web-platform/meta/clipboard-apis/async-interfaces.https.html.ini
testing/web-platform/meta/clipboard-apis/async-navigator-clipboard-basics.https.html.ini
toolkit/components/extensions/test/mochitest/mochitest-common.ini
toolkit/components/extensions/test/mochitest/test_ext_async_clipboard.html
--- a/dom/base/Navigator.cpp
+++ b/dom/base/Navigator.cpp
@@ -28,16 +28,17 @@
 #include "nsIScriptSecurityManager.h"
 #include "nsCharSeparatedTokenizer.h"
 #include "nsContentUtils.h"
 #include "nsUnicharUtils.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/Telemetry.h"
 #include "BatteryManager.h"
 #include "mozilla/dom/CredentialsContainer.h"
+#include "mozilla/dom/Clipboard.h"
 #include "mozilla/dom/GamepadServiceTest.h"
 #include "mozilla/dom/MediaCapabilities.h"
 #include "mozilla/dom/WakeLock.h"
 #include "mozilla/dom/power/PowerManagerService.h"
 #include "mozilla/dom/MIDIAccessManager.h"
 #include "mozilla/dom/MIDIOptionsBinding.h"
 #include "mozilla/dom/Permissions.h"
 #include "mozilla/dom/Presentation.h"
@@ -1801,16 +1802,25 @@ Navigator::MediaCapabilities()
 {
   if (!mMediaCapabilities) {
     mMediaCapabilities =
       new dom::MediaCapabilities(GetWindow()->AsGlobal());
   }
   return mMediaCapabilities;
 }
 
+Clipboard*
+Navigator::Clipboard()
+{
+  if (!mClipboard) {
+    mClipboard = new dom::Clipboard(GetWindow());
+  }
+  return mClipboard;
+}
+
 /* static */
 bool
 Navigator::Webdriver()
 {
   return Preferences::GetBool("marionette.enabled", false);
 }
 
 } // namespace dom
--- a/dom/base/Navigator.h
+++ b/dom/base/Navigator.h
@@ -35,16 +35,17 @@ class Geolocation;
 class systemMessageCallback;
 class MediaDevices;
 struct MediaStreamConstraints;
 class WakeLock;
 class ArrayBufferOrArrayBufferViewOrBlobOrFormDataOrUSVStringOrURLSearchParams;
 class ServiceWorkerContainer;
 class DOMRequest;
 class CredentialsContainer;
+class Clipboard;
 } // namespace dom
 } // namespace mozilla
 
 //*****************************************************************************
 // Navigator: Script "navigator" object
 //*****************************************************************************
 
 namespace mozilla {
@@ -202,16 +203,17 @@ public:
                               NavigatorUserMediaErrorCallback& aOnError,
                               uint64_t aInnerWindowID,
                               const nsAString& aCallID,
                               ErrorResult& aRv);
 
   already_AddRefed<ServiceWorkerContainer> ServiceWorker();
 
   mozilla::dom::CredentialsContainer* Credentials();
+  dom::Clipboard* Clipboard();
 
   static bool Webdriver();
 
   void GetLanguages(nsTArray<nsString>& aLanguages);
 
   StorageManager* Storage();
 
   static void GetAcceptLanguages(nsTArray<nsString>& aLanguages);
@@ -263,16 +265,17 @@ private:
   RefPtr<nsMimeTypeArray> mMimeTypes;
   RefPtr<nsPluginArray> mPlugins;
   RefPtr<Permissions> mPermissions;
   RefPtr<Geolocation> mGeolocation;
   RefPtr<battery::BatteryManager> mBatteryManager;
   RefPtr<Promise> mBatteryPromise;
   RefPtr<network::Connection> mConnection;
   RefPtr<CredentialsContainer> mCredentials;
+  RefPtr<dom::Clipboard> mClipboard;
   RefPtr<MediaDevices> mMediaDevices;
   RefPtr<ServiceWorkerContainer> mServiceWorkerContainer;
   nsCOMPtr<nsPIDOMWindowInner> mWindow;
   RefPtr<Presentation> mPresentation;
   RefPtr<GamepadServiceTest> mGamepadServiceTest;
   nsTArray<RefPtr<Promise> > mVRGetDisplaysPromises;
   RefPtr<VRServiceTest> mVRServiceTest;
   nsTArray<uint32_t> mRequestedVibrationPattern;
--- a/dom/bindings/Bindings.conf
+++ b/dom/bindings/Bindings.conf
@@ -138,16 +138,20 @@ DOMInterfaces = {
 'ChannelWrapper': {
     'nativeType': 'mozilla::extensions::ChannelWrapper',
 },
 
 'CharacterData': {
     'concrete': False
 },
 
+'Clipboard' : {
+    'implicitJSContext' : ['write', 'writeText', 'read', 'readText'],
+},
+
 'console': {
     'nativeType': 'mozilla::dom::Console',
 },
 
 'ConsoleInstance': {
     'implicitJSContext': ['clear', 'count', 'countReset', 'groupEnd', 'time', 'timeEnd'],
 },
 
new file mode 100644
--- /dev/null
+++ b/dom/events/Clipboard.cpp
@@ -0,0 +1,220 @@
+/* -*- 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/AbstractThread.h"
+#include "mozilla/dom/Clipboard.h"
+#include "mozilla/dom/ClipboardBinding.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/DataTransfer.h"
+#include "mozilla/dom/DataTransferItemList.h"
+#include "mozilla/dom/DataTransferItem.h"
+#include "mozilla/dom/ContentChild.h"
+#include "nsIClipboard.h"
+#include "nsISupportsPrimitives.h"
+#include "nsComponentManagerUtils.h"
+#include "nsITransferable.h"
+#include "nsArrayUtils.h"
+
+
+static mozilla::LazyLogModule gClipboardLog("Clipboard");
+
+namespace mozilla {
+namespace dom {
+
+Clipboard::Clipboard(nsPIDOMWindowInner* aWindow)
+: DOMEventTargetHelper(aWindow)
+{
+}
+
+Clipboard::~Clipboard()
+{
+}
+
+already_AddRefed<Promise>
+Clipboard::ReadHelper(JSContext* aCx, nsIPrincipal& aSubjectPrincipal,
+                      ClipboardReadType aClipboardReadType, ErrorResult& aRv)
+{
+  // Create a new promise
+  RefPtr<Promise> p = dom::Promise::Create(GetOwnerGlobal(), aRv);
+  if (aRv.Failed()) {
+    return nullptr;
+  }
+
+  // We want to disable security check for automated tests that have the pref
+  //  dom.events.testing.asyncClipboard set to true
+  if (!IsTestingPrefEnabled() && !nsContentUtils::PrincipalHasPermission(&aSubjectPrincipal,
+                                                         nsGkAtoms::clipboardRead)) {
+    MOZ_LOG(GetClipboardLog(), LogLevel::Debug, ("Clipboard, ReadHelper, "
+            "Don't have permissions for reading\n"));
+    p->MaybeRejectWithUndefined();
+    return p.forget();
+  }
+
+  // Want isExternal = true in order to use the data transfer object to perform a read
+  RefPtr<DataTransfer> dataTransfer = new DataTransfer(this, ePaste, /* is external */ true,
+                                                       nsIClipboard::kGlobalClipboard);
+
+  // Create a new runnable
+  RefPtr<nsIRunnable> r = NS_NewRunnableFunction(
+    "Clipboard::Read",
+    [p, dataTransfer, &aSubjectPrincipal, aClipboardReadType]() {
+      IgnoredErrorResult ier;
+      switch (aClipboardReadType) {
+        case eRead:
+          MOZ_LOG(GetClipboardLog(), LogLevel::Debug,
+                  ("Clipboard, ReadHelper, read case\n"));
+          dataTransfer->FillAllExternalData();
+          // If there are items on the clipboard, data transfer will contain those,
+          // else, data transfer will be empty and we will be resolving with an empty data transfer
+          p->MaybeResolve(dataTransfer);
+          break;
+        case eReadText:
+          MOZ_LOG(GetClipboardLog(), LogLevel::Debug,
+                  ("Clipboard, ReadHelper, read text case\n"));
+          nsAutoString str;
+          dataTransfer->GetData(NS_LITERAL_STRING(kTextMime), str, aSubjectPrincipal, ier);
+          // Either resolve with a string extracted from data transfer item
+          // or resolve with an empty string if nothing was found
+          p->MaybeResolve(str);
+          break;
+      }
+    });
+  // Dispatch the runnable
+  GetParentObject()->Dispatch(TaskCategory::Other, r.forget());
+  return p.forget();
+}
+
+already_AddRefed<Promise>
+Clipboard::Read(JSContext* aCx, nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv)
+{
+  return ReadHelper(aCx, aSubjectPrincipal, eRead, aRv);
+}
+
+already_AddRefed<Promise>
+Clipboard::ReadText(JSContext* aCx, nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv)
+{
+  return ReadHelper(aCx, aSubjectPrincipal, eReadText, aRv);
+}
+
+already_AddRefed<Promise>
+Clipboard::Write(JSContext* aCx, DataTransfer& aData, nsIPrincipal& aSubjectPrincipal,
+                 ErrorResult& aRv)
+{
+  // Create a promise
+  RefPtr<Promise> p = dom::Promise::Create(GetOwnerGlobal(), aRv);
+  if (aRv.Failed()) {
+    return nullptr;
+  }
+
+  // We want to disable security check for automated tests that have the pref
+  //  dom.events.testing.asyncClipboard set to true
+  if (!IsTestingPrefEnabled() && !nsContentUtils::IsCutCopyAllowed(&aSubjectPrincipal)) {
+    MOZ_LOG(GetClipboardLog(), LogLevel::Debug,
+            ("Clipboard, Write, Not allowed to write to clipboard\n"));
+    p->MaybeRejectWithUndefined();
+    return p.forget();
+  }
+
+  // Get the clipboard service
+  nsCOMPtr<nsIClipboard> clipboard(do_GetService("@mozilla.org/widget/clipboard;1"));
+  if (!clipboard) {
+    p->MaybeRejectWithUndefined();
+    return p.forget();
+  }
+
+  nsPIDOMWindowInner* owner = GetOwner();
+  nsIDocument* doc          = owner ? owner->GetDoc() : nullptr;
+  nsILoadContext* context   = doc ? doc->GetLoadContext() : nullptr;
+  if (!context) {
+    p->MaybeRejectWithUndefined();
+    return p.forget();
+  }
+
+  // Get the transferable
+  RefPtr<nsITransferable> transferable = aData.GetTransferable(0, context);
+  if (!transferable) {
+    p->MaybeRejectWithUndefined();
+    return p.forget();
+  }
+
+  // Create a runnable
+  RefPtr<nsIRunnable> r = NS_NewRunnableFunction(
+    "Clipboard::Write",
+    [transferable, p, clipboard]() {
+      nsresult rv = clipboard->SetData(transferable,
+                                       /* owner of the transferable */ nullptr,
+                                       nsIClipboard::kGlobalClipboard);
+      if (NS_FAILED(rv)) {
+        p->MaybeRejectWithUndefined();
+        return;
+      }
+      p->MaybeResolveWithUndefined();
+      return;
+    });
+  // Dispatch the runnable
+  GetParentObject()->Dispatch(TaskCategory::Other, r.forget());
+  return p.forget();
+}
+
+already_AddRefed<Promise>
+Clipboard::WriteText(JSContext* aCx, const nsAString& aData,
+                     nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv)
+{
+  // We create a data transfer with text/plain format so that
+  //  we can reuse Clipboard::Write(...) member function
+  RefPtr<DataTransfer> dataTransfer = new DataTransfer(this, eCopy,
+                                                      /* is external */ true,
+                                                      /* clipboard type */ -1);
+  dataTransfer->SetData(NS_LITERAL_STRING(kTextMime), aData, aSubjectPrincipal, aRv);
+  return Write(aCx, *dataTransfer, aSubjectPrincipal, aRv);
+}
+
+JSObject*
+Clipboard::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
+{
+  return Clipboard_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+/* static */ LogModule*
+Clipboard::GetClipboardLog()
+{
+  return gClipboardLog;
+}
+
+bool
+Clipboard::IsTestingPrefEnabled()
+{
+  static bool sPrefCached = false;
+  static bool sPrefCacheValue = false;
+
+  if (!sPrefCached) {
+    sPrefCached = true;
+    Preferences::AddBoolVarCache(&sPrefCacheValue, "dom.events.testing.asyncClipboard");
+  }
+  MOZ_LOG(GetClipboardLog(), LogLevel::Debug,
+            ("Clipboard, Is testing enabled? %d\n", sPrefCacheValue));
+  return sPrefCacheValue;
+}
+
+
+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 dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/events/Clipboard.h
@@ -0,0 +1,68 @@
+/* -*- 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/. */
+
+#ifndef mozilla_dom_Clipboard_h_
+#define mozilla_dom_Clipboard_h_
+
+#include "nsString.h"
+#include "mozilla/DOMEventTargetHelper.h"
+#include "mozilla/Logging.h"
+#include "mozilla/dom/DataTransfer.h"
+
+namespace mozilla {
+namespace dom {
+
+enum ClipboardReadType {
+  eRead,
+  eReadText,
+};
+
+class Promise;
+
+// https://www.w3.org/TR/clipboard-apis/#clipboard-interface
+class Clipboard : public DOMEventTargetHelper
+{
+public:
+  NS_DECL_ISUPPORTS_INHERITED
+  NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(Clipboard,
+                                           DOMEventTargetHelper)
+
+  IMPL_EVENT_HANDLER(message)
+  IMPL_EVENT_HANDLER(messageerror)
+
+  explicit Clipboard(nsPIDOMWindowInner* aWindow);
+  already_AddRefed<Promise> Read(JSContext* aCx, nsIPrincipal& aSubjectPrincipal,
+                                 ErrorResult& aRv);
+  already_AddRefed<Promise> ReadText(JSContext* aCx, nsIPrincipal& aSubjectPrincipal,
+                                     ErrorResult& aRv);
+  already_AddRefed<Promise> Write(JSContext* aCx, DataTransfer& aData,
+                                  nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv);
+  already_AddRefed<Promise> WriteText(JSContext* aCx, const nsAString& aData,
+                                    nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv);
+
+  static LogModule* GetClipboardLog();
+
+
+  virtual JSObject*
+  WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
+
+private:
+  // Checks if dom.events.testing.asyncClipboard pref is enabled.
+  // The aforementioned pref allows automated tests to bypass the security checks when writing to
+  //  or reading from the clipboard.
+  bool IsTestingPrefEnabled();
+
+  already_AddRefed<Promise> ReadHelper(JSContext* aCx, nsIPrincipal& aSubjectPrincipal,
+                                       ClipboardReadType aClipboardReadType, ErrorResult& aRv);
+
+  ~Clipboard();
+
+
+};
+
+} // namespace dom
+} // namespace mozilla
+#endif // mozilla_dom_Clipboard_h_
--- a/dom/events/DataTransfer.cpp
+++ b/dom/events/DataTransfer.cpp
@@ -1172,38 +1172,34 @@ DataTransfer::ConvertFromVariant(nsIVari
       ptrSupports.forget(aSupports);
 
       *aLength = sizeof(nsISupportsInterfacePointer *);
     }
 
     return true;
   }
 
-  char16_t* chrs;
-  uint32_t len = 0;
-  nsresult rv = aVariant->GetAsWStringWithSize(&len, &chrs);
+  nsAutoString str;
+  nsresult rv = aVariant->GetAsAString(str);
   if (NS_FAILED(rv)) {
     return false;
   }
 
-  nsAutoString str;
-  str.Adopt(chrs, len);
-
   nsCOMPtr<nsISupportsString>
     strSupports(do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID));
   if (!strSupports) {
     return false;
   }
 
   strSupports->SetData(str);
 
   strSupports.forget(aSupports);
 
   // each character is two bytes
-  *aLength = str.Length() << 1;
+  *aLength = str.Length() * 2;
 
   return true;
 }
 
 void
 DataTransfer::Disconnect()
 {
   SetMode(Mode::Protected);
@@ -1381,19 +1377,17 @@ void
 DataTransfer::CacheExternalClipboardFormats(bool aPlainTextOnly)
 {
   // Called during the constructor for paste events to cache the formats
   // available on the clipboard. As with CacheExternalDragFormats, the
   // data will only be retrieved when needed.
   NS_ASSERTION(mEventMessage == ePaste,
                "caching clipboard data for invalid event");
 
-  nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager();
-  nsCOMPtr<nsIPrincipal> sysPrincipal;
-  ssm->GetSystemPrincipal(getter_AddRefs(sysPrincipal));
+  nsCOMPtr<nsIPrincipal> sysPrincipal = nsContentUtils::GetSystemPrincipal();
 
   nsTArray<nsCString> typesArray;
 
   if (XRE_IsContentProcess()) {
     ContentChild::GetSingleton()->SendGetExternalClipboardFormats(mClipboardType, aPlainTextOnly, &typesArray);
   } else {
     GetExternalClipboardFormats(mClipboardType, aPlainTextOnly, &typesArray);
   }
--- a/dom/events/DataTransfer.h
+++ b/dom/events/DataTransfer.h
@@ -446,16 +446,17 @@ protected:
   nsresult GetDataAtInternal(const nsAString& aFormat, uint32_t aIndex,
                              nsIPrincipal* aSubjectPrincipal,
                              nsIVariant** aData);
 
   nsresult SetDataAtInternal(const nsAString& aFormat, nsIVariant* aData,
                              uint32_t aIndex, nsIPrincipal* aSubjectPrincipal);
 
   friend class ContentParent;
+  friend class Clipboard;
 
   void FillAllExternalData();
 
   void FillInExternalCustomTypes(uint32_t aIndex, nsIPrincipal* aPrincipal);
 
   void FillInExternalCustomTypes(nsIVariant* aData, uint32_t aIndex,
                                  nsIPrincipal* aPrincipal);
 
--- a/dom/events/DataTransferItem.h
+++ b/dom/events/DataTransferItem.h
@@ -98,16 +98,17 @@ public:
     return mPrincipal;
   }
   void SetPrincipal(nsIPrincipal* aPrincipal)
   {
     mPrincipal = aPrincipal;
   }
 
   already_AddRefed<nsIVariant> DataNoSecurityCheck();
+  // Data may return null if the clipboard state has changed since the type was detected.
   already_AddRefed<nsIVariant> Data(nsIPrincipal* aPrincipal, ErrorResult& aRv);
 
   // Note: This can modify the mKind.  Callers of this method must let the
   // relevant DataTransfer know, because its types list can change as a result.
   void SetData(nsIVariant* aData);
 
   uint32_t Index() const
   {
--- a/dom/events/DataTransferItemList.cpp
+++ b/dom/events/DataTransferItemList.cpp
@@ -147,17 +147,18 @@ DataTransferItemList::Add(const nsAStrin
                           const nsAString& aType,
                           nsIPrincipal& aSubjectPrincipal,
                           ErrorResult& aRv)
 {
   if (NS_WARN_IF(mDataTransfer->IsReadOnly())) {
     return nullptr;
   }
 
-  nsCOMPtr<nsIVariant> data(new storage::TextVariant(aData));
+  RefPtr<nsVariantCC> data(new nsVariantCC());
+  data->SetAsAString(aData);
 
   nsAutoString format;
   mDataTransfer->GetRealFormat(aType, format);
 
   if (!DataTransfer::PrincipalMaySetData(format, data, &aSubjectPrincipal)) {
     aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
     return nullptr;
   }
--- a/dom/events/Event.cpp
+++ b/dom/events/Event.cpp
@@ -4,16 +4,17 @@
  * 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 "AccessCheck.h"
 #include "base/basictypes.h"
 #include "ipc/IPCMessageUtils.h"
 #include "mozilla/dom/Event.h"
 #include "mozilla/dom/ShadowRoot.h"
+#include "mozilla/EventDispatcher.h"
 #include "mozilla/ContentEvents.h"
 #include "mozilla/DOMEventTargetHelper.h"
 #include "mozilla/EventStateManager.h"
 #include "mozilla/InternalMutationEvent.h"
 #include "mozilla/dom/Performance.h"
 #include "mozilla/dom/WorkerPrivate.h"
 #include "mozilla/MiscEvents.h"
 #include "mozilla/MouseEvents.h"
--- a/dom/events/moz.build
+++ b/dom/events/moz.build
@@ -42,16 +42,17 @@ EXPORTS.mozilla += [
     'TextComposition.h',
     'VirtualKeyCodeList.h',
     'WheelHandlingHelper.h',
 ]
 
 EXPORTS.mozilla.dom += [
     'AnimationEvent.h',
     'BeforeUnloadEvent.h',
+    'Clipboard.h',
     'ClipboardEvent.h',
     'CommandEvent.h',
     'CompositionEvent.h',
     'ConstructibleEventTarget.h',
     'CustomEvent.h',
     'DataTransfer.h',
     'DataTransferItem.h',
     'DataTransferItemList.h',
@@ -85,16 +86,17 @@ EXPORTS.mozilla.dom += [
 
 if CONFIG['MOZ_WEBSPEECH']:
     EXPORTS.mozilla.dom += ['SpeechRecognitionError.h']
 
 UNIFIED_SOURCES += [
     'AnimationEvent.cpp',
     'AsyncEventDispatcher.cpp',
     'BeforeUnloadEvent.cpp',
+    'Clipboard.cpp',
     'ClipboardEvent.cpp',
     'CommandEvent.cpp',
     'CompositionEvent.cpp',
     'ConstructibleEventTarget.cpp',
     'ContentEventHandler.cpp',
     'CustomEvent.cpp',
     'DataTransfer.cpp',
     'DataTransferItem.cpp',
new file mode 100644
--- /dev/null
+++ b/dom/webidl/Clipboard.webidl
@@ -0,0 +1,24 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/.
+ *
+ * The origin of this IDL file is
+ * http://www.w3.org/TR/geolocation-API
+ *
+ * Copyright © 2018 W3C® (MIT, ERCIM, Keio), All Rights Reserved. W3C
+ * liability, trademark and document use rules apply.
+ */
+
+
+[SecureContext, Exposed=Window, Pref="dom.events.asyncClipboard"]
+interface Clipboard : EventTarget {
+  [Pref="dom.events.asyncClipboard.dataTransfer", Throws, NeedsSubjectPrincipal]
+  Promise<DataTransfer> read();
+  [Throws, NeedsSubjectPrincipal]
+  Promise<DOMString> readText();
+  [Pref="dom.events.asyncClipboard.dataTransfer", Throws, NeedsSubjectPrincipal]
+  Promise<void> write(DataTransfer data);
+  [Throws, NeedsSubjectPrincipal]
+  Promise<void> writeText(DOMString data);
+};
\ No newline at end of file
--- a/dom/webidl/Navigator.webidl
+++ b/dom/webidl/Navigator.webidl
@@ -326,8 +326,14 @@ partial interface Navigator {
 };
 
 // https://w3c.github.io/webdriver/webdriver-spec.html#interface
 [NoInterfaceObject]
 interface NavigatorAutomationInformation {
   [Pref="dom.webdriver.enabled"]
   readonly attribute boolean webdriver;
 };
+
+// https://www.w3.org/TR/clipboard-apis/#navigator-interface
+partial interface Navigator {
+  [Pref="dom.events.asyncClipboard", SecureContext, SameObject]
+  readonly attribute Clipboard clipboard;
+};
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -53,16 +53,19 @@ with Files("Caret*"):
     BUG_COMPONENT = ("Core", "Editor")
 
 with Files("Channel*"):
     BUG_COMPONENT = ("Core", "Web Audio")
 
 with Files("Client*"):
     BUG_COMPONENT = ("Core", "DOM: Service Workers")
 
+with Files("Clipboard.webidl"):
+    BUG_COMPONENT = ("Core", "DOM: Events")
+
 with Files("ClipboardEvent.webidl"):
     BUG_COMPONENT = ("Core", "DOM: Events")
 
 with Files("ConstantSourceNode.webidl"):
     BUG_COMPONENT = ("Core", "Web Audio")
 
 with Files("ConvolverNode.webidl"):
     BUG_COMPONENT = ("Core", "Web Audio")
@@ -414,16 +417,17 @@ WEBIDL_FILES = [
     'ChannelSplitterNode.webidl',
     'CharacterData.webidl',
     'CheckerboardReportService.webidl',
     'ChildNode.webidl',
     'ChildSHistory.webidl',
     'ChromeNodeList.webidl',
     'Client.webidl',
     'Clients.webidl',
+    'Clipboard.webidl',
     'ClipboardEvent.webidl',
     'CommandEvent.webidl',
     'Comment.webidl',
     'CompositionEvent.webidl',
     'Console.webidl',
     'ConstantSourceNode.webidl',
     'ConvolverNode.webidl',
     'Coordinates.webidl',
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -5847,8 +5847,14 @@ pref("general.document_open_conversion_d
 // documentElement and document.body are passive by default.
 pref("dom.event.default_to_passive_touch_listeners", true);
 
 // Enable FastBlock?
 pref("browser.fastblock.enabled", false);
 // The timeout (ms) since navigation start, all tracker connections been made
 // after this timeout will be canceled.
 pref("browser.fastblock.timeout", 5000);
+
+// Disables clipboard reads and writes by default.
+pref("dom.events.asyncClipboard", false);
+pref("dom.events.asyncClipboard.dataTransfer", false);
+// Should only be enabled in tests
+pref("dom.events.testing.asyncClipboard", false);
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/meta/clipboard-apis/__dir__.ini
@@ -0,0 +1,1 @@
+prefs: [dom.events.asyncClipboard:true, dom.events.asyncClipboard.dataTransfer:true, dom.events.testing.asyncClipboard:true]
--- a/testing/web-platform/meta/clipboard-apis/async-interfaces.https.html.ini
+++ b/testing/web-platform/meta/clipboard-apis/async-interfaces.https.html.ini
@@ -1,79 +1,4 @@
 [async-interfaces.https.html]
-  [Navigator interface: attribute clipboard]
-    expected: FAIL
-
-  [Navigator interface: navigator must inherit property "clipboard" with the proper type (0)]
-    expected: FAIL
-
-  [Clipboard interface: existence and properties of interface object]
-    expected: FAIL
-
-  [Clipboard interface object length]
-    expected: FAIL
-
-  [Clipboard interface object name]
-    expected: FAIL
-
-  [Clipboard interface: existence and properties of interface prototype object]
-    expected: FAIL
-
-  [Clipboard interface: existence and properties of interface prototype object's "constructor" property]
-    expected: FAIL
-
-  [Clipboard interface: existence and properties of interface prototype object's @@unscopables property]
-    expected: FAIL
-
-  [Clipboard interface: operation read()]
-    expected: FAIL
-
-  [Clipboard interface: operation readText()]
-    expected: FAIL
-
-  [Clipboard interface: operation write(DataTransfer)]
-    expected: FAIL
-
-  [Clipboard interface: operation writeText(DOMString)]
-    expected: FAIL
-
-  [Clipboard must be primary interface of navigator.clipboard]
-    expected: FAIL
-
-  [Stringification of navigator.clipboard]
-    expected: FAIL
-
-  [Clipboard interface: navigator.clipboard must inherit property "read" with the proper type (0)]
-    expected: FAIL
-
-  [Clipboard interface: navigator.clipboard must inherit property "readText" with the proper type (1)]
-    expected: FAIL
-
-  [Clipboard interface: navigator.clipboard must inherit property "write" with the proper type (2)]
-    expected: FAIL
-
-  [Clipboard interface: calling write(DataTransfer) on navigator.clipboard with too few arguments must throw TypeError]
-    expected: FAIL
-
-  [Clipboard interface: navigator.clipboard must inherit property "writeText" with the proper type (3)]
-    expected: FAIL
-
-  [Clipboard interface: calling writeText(DOMString) on navigator.clipboard with too few arguments must throw TypeError]
-    expected: FAIL
-
-  [Navigator interface: navigator must inherit property "clipboard" with the proper type]
-    expected: FAIL
-
-  [Clipboard interface: navigator.clipboard must inherit property "read()" with the proper type]
-    expected: FAIL
-
-  [Clipboard interface: navigator.clipboard must inherit property "readText()" with the proper type]
-    expected: FAIL
-
-  [Clipboard interface: navigator.clipboard must inherit property "write(DataTransfer)" with the proper type]
-    expected: FAIL
-
-  [Clipboard interface: navigator.clipboard must inherit property "writeText(DOMString)" with the proper type]
-    expected: FAIL
-
   [ClipboardEvent interface: new ClipboardEvent("x") must inherit property "clipboardData" with the proper type]
     expected: FAIL
 
deleted file mode 100644
--- a/testing/web-platform/meta/clipboard-apis/async-navigator-clipboard-basics.https.html.ini
+++ /dev/null
@@ -1,28 +0,0 @@
-[async-navigator-clipboard-basics.https.html]
-  [navigator.clipboard exists]
-    expected: FAIL
-
-  [navigator.clipboard.write(DataTransfer) succeeds]
-    expected: FAIL
-
-  [navigator.clipboard.write() fails (expect DataTransfer)]
-    expected: FAIL
-
-  [navigator.clipboard.write(null) fails (expect DataTransfer)]
-    expected: FAIL
-
-  [navigator.clipboard.write(DOMString) fails (expect DataTransfer)]
-    expected: FAIL
-
-  [navigator.clipboard.writeText(DOMString) succeeds]
-    expected: FAIL
-
-  [navigator.clipboard.writeText() fails (expect DOMString)]
-    expected: FAIL
-
-  [navigator.clipboard.read() succeeds]
-    expected: FAIL
-
-  [navigator.clipboard.readText() succeeds]
-    expected: FAIL
-
--- a/toolkit/components/extensions/test/mochitest/mochitest-common.ini
+++ b/toolkit/components/extensions/test/mochitest/mochitest-common.ini
@@ -50,16 +50,17 @@ support-files =
   webrequest_worker.js
   !/dom/tests/mochitest/geolocation/network_geolocation.sjs
   !/toolkit/components/passwordmgr/test/authenticate.sjs
   file_redirect_data_uri.html
 prefs =
   security.mixed_content.upgrade_display_content=false
   browser.chrome.guess_favicon=true
 
+[test_ext_async_clipboard.html]
 [test_ext_background_canvas.html]
 [test_ext_background_page.html]
 skip-if = (toolkit == 'android') # android doesn't have devtools
 [test_ext_canvas_resistFingerprinting.html]
 [test_ext_clipboard.html]
 [test_ext_clipboard_image.html]
 skip-if = headless # disabled test case with_permission_allow_copy, see inline comment. Headless: Bug 1405872
 [test_ext_contentscript_about_blank.html]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_async_clipboard.html
@@ -0,0 +1,370 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Async Clipboard permissions tests</title>
+  <script src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script src="/tests/SimpleTest/AddTask.js"></script>
+  <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+  <script src="head.js"></script>
+  <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+
+<script>
+"use strict";
+
+/* globals clipboardWriteText, clipboardWrite, clipboardReadText, clipboardRead */
+function shared() {
+  this.clipboardWriteText = function(txt) {
+    return navigator.clipboard.writeText(txt);
+  };
+
+  this.clipboardWrite = function(dt) {
+    return navigator.clipboard.write(dt);
+  };
+
+  this.clipboardReadText = function() {
+    return navigator.clipboard.readText();
+  };
+
+  this.clipboardRead = function() {
+    return navigator.clipboard.read();
+  };
+}
+
+/**
+ * Clear the clipboard.
+ *
+ * This is needed because Services.clipboard.emptyClipboard() does not clear the actual system clipboard.
+ */
+function clearClipboard() {
+  if (AppConstants.platform == "android") {
+    // On android, this clears the actual system clipboard
+    SpecialPowers.Services.clipboard.emptyClipboard(SpecialPowers.Services.clipboard.kGlobalClipboard);
+    return;
+  }
+  // Need to do this hack on other platforms to clear the actual system clipboard
+  let transf = SpecialPowers.Cc["@mozilla.org/widget/transferable;1"]
+    .createInstance(SpecialPowers.Ci.nsITransferable);
+  transf.init(null);
+  // Empty transferables may cause crashes, so just add an unknown type.
+  const TYPE = "text/x-moz-place-empty";
+  transf.addDataFlavor(TYPE);
+  transf.setTransferData(TYPE, {}, 0);
+  SpecialPowers.Services.clipboard.setData(transf, null, SpecialPowers.Services.clipboard.kGlobalClipboard);
+}
+
+add_task(async function setup() {
+  await SpecialPowers.pushPrefEnv({"set": [
+    ["dom.events.asyncClipboard", true],
+    ["dom.events.asyncClipboard.dataTransfer", true],
+  ]});
+});
+
+// Test that without enough permissions, we are NOT allowed to use writeText, write, read or readText in background script
+add_task(async function test_background_async_clipboard_no_permissions() {
+  function backgroundScript() {
+    let dt = new DataTransfer();
+    dt.items.add("Howdy", "text/plain");
+    browser.test.assertRejects(clipboardRead(), undefined, "Read should be denied without permission");
+    browser.test.assertRejects(clipboardWrite(dt), undefined, "Write should be denied without permission");
+    browser.test.assertRejects(clipboardWriteText("blabla"), undefined, "WriteText should be denied without permission");
+    browser.test.assertRejects(clipboardReadText(), undefined, "ReadText should be denied without permission");
+    browser.test.sendMessage("ready");
+  }
+  let extensionData = {
+    background: [shared, backgroundScript],
+  };
+  let extension = ExtensionTestUtils.loadExtension(extensionData);
+  await extension.startup();
+  await extension.awaitMessage("ready");
+  await extension.unload();
+});
+
+// Test that without enough permissions, we are NOT allowed to use writeText, write, read or readText in content script
+add_task(async function test_contentscript_async_clipboard_no_permission() {
+  function contentScript() {
+    let dt = new DataTransfer();
+    dt.items.add("Howdy", "text/plain");
+    browser.test.assertRejects(clipboardRead(), undefined, "Read should be denied without permission");
+    browser.test.assertRejects(clipboardWrite(dt), undefined, "Write should be denied without permission");
+    browser.test.assertRejects(clipboardWriteText("blabla"), undefined, "WriteText should be denied without permission");
+    browser.test.assertRejects(clipboardReadText(), undefined, "ReadText should be denied without permission");
+    browser.test.sendMessage("ready");
+  }
+  let extensionData = {
+    manifest: {
+      content_scripts: [{
+        js: ["shared.js", "contentscript.js"],
+        matches: ["https://example.com/*/file_sample.html"],
+      }],
+    },
+    files: {
+      "shared.js": shared,
+      "contentscript.js": contentScript,
+    },
+  };
+  let extension = ExtensionTestUtils.loadExtension(extensionData);
+  await extension.startup();
+  let win = window.open("https://example.com/tests/toolkit/components/extensions/test/mochitest/test-oop-extensions/file_sample.html");
+  await extension.awaitMessage("ready");
+  await extension.unload();
+  win.close();
+});
+
+// Test that with enough permissions, we are allowed to use writeText  in content script
+add_task(async function test_contentscript_clipboard_permission_writetext() {
+  function contentScript() {
+    let str = "HI";
+    clipboardWriteText(str).then(function() {
+      // nothing here
+      browser.test.sendMessage("ready");
+    }, function(err) {
+      browser.test.fail("WriteText promise rejected");
+      browser.test.sendMessage("ready");
+    }); // clipboardWriteText
+  }
+  let extensionData = {
+    manifest: {
+      content_scripts: [{
+        js: ["shared.js", "contentscript.js"],
+        matches: ["https://example.com/*/file_sample.html"],
+      }],
+      permissions: [
+        "clipboardWrite",
+        "clipboardRead",
+      ],
+    },
+    files: {
+      "shared.js": shared,
+      "contentscript.js": contentScript,
+    },
+  };
+  let extension = ExtensionTestUtils.loadExtension(extensionData);
+  await extension.startup();
+  let win = window.open("https://example.com/tests/toolkit/components/extensions/test/mochitest/test-oop-extensions/file_sample.html");
+  await extension.awaitMessage("ready");
+  const actual = SpecialPowers.getClipboardData("text/unicode");
+  is(actual, "HI", "right string copied by write");
+  await extension.unload();
+  win.close();
+});
+
+// Test that with enough permissions, we are allowed to use readText in content script
+add_task(async function test_contentscript_clipboard_permission_readtext() {
+  function contentScript() {
+    let str = "HI";
+    clipboardReadText().then(function(strData) {
+      if (strData == str) {
+        browser.test.succeed("Successfully read from clipboard");
+      } else {
+        browser.test.fail("ReadText read the wrong thing from clipboard:" + strData);
+      }
+      browser.test.sendMessage("ready");
+    }, function(err) {
+      browser.test.fail("ReadText promise rejected");
+      browser.test.sendMessage("ready");
+    }); // clipboardReadText
+  }
+  let extensionData = {
+    manifest: {
+      content_scripts: [{
+        js: ["shared.js", "contentscript.js"],
+        matches: ["https://example.com/*/file_sample.html"],
+      }],
+      permissions: [
+        "clipboardWrite",
+        "clipboardRead",
+      ],
+    },
+    files: {
+      "shared.js": shared,
+      "contentscript.js": contentScript,
+    },
+  };
+  SpecialPowers.clipboardCopyString("HI");
+  let extension = ExtensionTestUtils.loadExtension(extensionData);
+  await extension.startup();
+  let win = window.open("https://example.com/tests/toolkit/components/extensions/test/mochitest/test-oop-extensions/file_sample.html");
+  await extension.awaitMessage("ready");
+  await extension.unload();
+  win.close();
+});
+
+// Test that with enough permissions, we are allowed to use write in content script
+add_task(async function test_contentscript_clipboard_permission_write() {
+  function contentScript() {
+    let str = "HI";
+    let dt = new DataTransfer();
+    dt.items.add(str, "text/plain");
+    clipboardWrite(dt).then(function() {
+      // nothing here
+      browser.test.sendMessage("ready");
+    }, function(err) { // clipboardWrite promise error function
+      browser.test.fail("Write promise rejected");
+      browser.test.sendMessage("ready");
+    }); // clipboard write
+  }
+  let extensionData = {
+    manifest: {
+      content_scripts: [{
+        js: ["shared.js", "contentscript.js"],
+        matches: ["https://example.com/*/file_sample.html"],
+      }],
+      permissions: [
+        "clipboardWrite",
+        "clipboardRead",
+      ],
+    },
+    files: {
+      "shared.js": shared,
+      "contentscript.js": contentScript,
+    },
+  };
+  let extension = ExtensionTestUtils.loadExtension(extensionData);
+  await extension.startup();
+  let win = window.open("https://example.com/tests/toolkit/components/extensions/test/mochitest/test-oop-extensions/file_sample.html");
+  await extension.awaitMessage("ready");
+  const actual = SpecialPowers.getClipboardData("text/unicode");
+  is(actual, "HI", "right string copied by write");
+  await extension.unload();
+  win.close();
+});
+
+// Test that with enough permissions, we are allowed to use read in content script
+add_task(async function test_contentscript_clipboard_permission_read() {
+  function contentScript() {
+    clipboardRead().then(function(dt) {
+      let s = dt.getData("text/plain");
+      if (s == "HELLO") {
+        browser.test.succeed("Read promise successfully read the right thing");
+      } else {
+        browser.test.fail("Read read the wrong string from clipboard:" + s);
+      }
+      browser.test.sendMessage("ready");
+    }, function(err) { // clipboardRead promise error function
+      browser.test.fail("Read promise rejected");
+      browser.test.sendMessage("ready");
+    }); // clipboard read
+  }
+  let extensionData = {
+    manifest: {
+      content_scripts: [{
+        js: ["shared.js", "contentscript.js"],
+        matches: ["https://example.com/*/file_sample.html"],
+      }],
+      permissions: [
+        "clipboardWrite",
+        "clipboardRead",
+      ],
+    },
+    files: {
+      "shared.js": shared,
+      "contentscript.js": contentScript,
+    },
+  };
+  let extension = ExtensionTestUtils.loadExtension(extensionData);
+  SpecialPowers.clipboardCopyString("HELLO");
+  await extension.startup();
+  let win = window.open("https://example.com/tests/toolkit/components/extensions/test/mochitest/test-oop-extensions/file_sample.html");
+  await extension.awaitMessage("ready");
+  await extension.unload();
+  win.close();
+});
+
+// Test that performing readText(...) when the clipboard is empty returns an empty string
+add_task(async function test_contentscript_clipboard_nocontents_readtext() {
+  function contentScript() {
+    clipboardReadText().then(function(strData) {
+      if (strData == "") {
+        browser.test.succeed("ReadText successfully read correct thing from an empty clipboard");
+      } else {
+        browser.test.fail("ReadText should have read an empty string, but read:" + strData);
+      }
+      browser.test.sendMessage("ready");
+    }, function(err) {
+      browser.test.fail("ReadText promise rejected: " + err);
+      browser.test.sendMessage("ready");
+    });
+  }
+  let extensionData = {
+    manifest: {
+      content_scripts: [{
+        js: ["shared.js", "contentscript.js"],
+        matches: ["https://example.com/*/file_sample.html"],
+      }],
+      permissions: [
+        "clipboardRead",
+      ],
+    },
+    files: {
+      "shared.js": shared,
+      "contentscript.js": contentScript,
+    },
+  };
+
+  await SimpleTest.promiseClipboardChange("", () => {
+    clearClipboard();
+  }, "text/x-moz-place-empty");
+  let extension = ExtensionTestUtils.loadExtension(extensionData);
+  await extension.startup();
+  let win = window.open("https://example.com/tests/toolkit/components/extensions/test/mochitest/test-oop-extensions/file_sample.html");
+  await extension.awaitMessage("ready");
+  await extension.unload();
+  win.close();
+});
+
+// Test that performing read(...) when the clipboard is empty returns an empty data transfer
+add_task(async function test_contentscript_clipboard_nocontents_read() {
+  function contentScript() {
+    clipboardRead().then(function(dataT) {
+      // On macOS if we clear the clipboard and read from it, there will be
+      // no items in the data transfer object.
+      // On linux with e10s enabled clearing of the clipboard does not happen in
+      // the same way as it does on other platforms. So when we clear  the clipboard
+      // and read from it, the data transfer object contains an item of type
+      // text/plain and kind string, but we can't call getAsString on it to verify
+      // that at least it is an empty string because the callback never gets invoked.
+      if (dataT.items.length == 0 ||
+        (dataT.items.length == 1 && dataT.items[0].type == "text/plain" &&
+         dataT.items[0].kind == "string")) {
+        browser.test.succeed("Read promise successfully resolved");
+      } else {
+        browser.test.fail("Read read the wrong thing from clipboard, " +
+          "data transfer has this many items:" + dataT.items.length);
+      }
+      browser.test.sendMessage("ready");
+    }, function(err) {
+      browser.test.fail("Read promise rejected: " + err);
+      browser.test.sendMessage("ready");
+    });
+  }
+  let extensionData = {
+    manifest: {
+      content_scripts: [{
+        js: ["shared.js", "contentscript.js"],
+        matches: ["https://example.com/*/file_sample.html"],
+      }],
+      permissions: [
+        "clipboardRead",
+      ],
+    },
+    files: {
+      "shared.js": shared,
+      "contentscript.js": contentScript,
+    },
+  };
+
+  await SimpleTest.promiseClipboardChange("", () => {
+    clearClipboard();
+  }, "text/x-moz-place-empty");
+  let extension = ExtensionTestUtils.loadExtension(extensionData);
+  await extension.startup();
+  let win = window.open("https://example.com/tests/toolkit/components/extensions/test/mochitest/test-oop-extensions/file_sample.html");
+  await extension.awaitMessage("ready");
+  await extension.unload();
+  win.close();
+});
+</script>
+</body>
+</html>
\ No newline at end of file