Bug 1461465 - Implement async Clipboard APIs, r=nika,r=enndeakin
authorAnny Gakhokidze <agakhokidze@mozilla.com>
Thu, 31 May 2018 11:57:57 -0400
changeset 429458 647fdd384a5e451b4b8db0e5be9796bf61f0082e
parent 429457 02395f0e8074d8a4d6a0de6963574ba83bee9027
child 429459 6bf87456771b83737bc433ae493f2bdfb39c7e61
push id34364
push userbtara@mozilla.com
push dateTue, 31 Jul 2018 21:59:26 +0000
treeherdermozilla-central@d57a89840dbb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnika, enndeakin
bugs1461465
milestone63.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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
@@ -29,16 +29,17 @@
 #include "nsCharSeparatedTokenizer.h"
 #include "nsContentUtils.h"
 #include "nsUnicharUtils.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/StaticPrefs.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"
@@ -1770,16 +1771,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
@@ -5848,8 +5848,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