Bug 1463919 - Add AutoplayRequest to encapsulate asking for autoplay permission. r=smaug draft
authorChris Pearce <cpearce@mozilla.com>
Fri, 22 Jun 2018 11:57:24 +1200
changeset 813230 a3db3a1ef87a09442b47ae1b1034b4a0143289fb
parent 812962 3cfc350101967376909ad3c729f9779ae0ab7a94
child 813231 f999b9a4a1ae7a5a7f1dd31efd3003e40d7fa102
push id114832
push userbmo:cpearce@mozilla.com
push dateMon, 02 Jul 2018 19:32:21 +0000
reviewerssmaug
bugs1463919
milestone63.0a1
Bug 1463919 - Add AutoplayRequest to encapsulate asking for autoplay permission. r=smaug Add an implementation of nsIContentPermissionRequest to encapsulate requesting permission from the user to autoplay audible media. All documents in the tab request permission using the top level document's origin, so the AutoplayRequest instance for a tab is stored on the top level content window of the tab. AutoplayRequest ensures that there's only a single prompt shown at once. MozReview-Commit-ID: 2u3aLnEa21z
dom/base/nsGlobalWindowInner.cpp
dom/base/nsPIDOMWindow.h
dom/html/AutoplayRequest.cpp
dom/html/AutoplayRequest.h
dom/html/moz.build
dom/media/AutoplayPolicy.cpp
dom/media/AutoplayPolicy.h
--- a/dom/base/nsGlobalWindowInner.cpp
+++ b/dom/base/nsGlobalWindowInner.cpp
@@ -12,16 +12,17 @@
 
 // Local Includes
 #include "Navigator.h"
 #include "nsContentSecurityManager.h"
 #include "nsScreen.h"
 #include "nsHistory.h"
 #include "nsDOMNavigationTiming.h"
 #include "nsIDOMStorageManager.h"
+#include "mozilla/dom/AutoplayRequest.h"
 #include "mozilla/dom/DOMJSProxyHandler.h"
 #include "mozilla/dom/DOMPrefs.h"
 #include "mozilla/dom/EventTarget.h"
 #include "mozilla/dom/LocalStorage.h"
 #include "mozilla/dom/Storage.h"
 #include "mozilla/dom/IdleRequest.h"
 #include "mozilla/dom/Performance.h"
 #include "mozilla/dom/StorageEvent.h"
@@ -8135,16 +8136,49 @@ nsPIDOMWindowInner::AsGlobal()
 }
 
 const nsIGlobalObject*
 nsPIDOMWindowInner::AsGlobal() const
 {
   return nsGlobalWindowInner::Cast(this);
 }
 
+static nsPIDOMWindowInner*
+GetTopLevelInnerWindow(nsPIDOMWindowInner* aWindow)
+{
+  if (!aWindow) {
+    return nullptr;
+  }
+  nsIDocShell* docShell = aWindow->GetDocShell();
+  if (!docShell) {
+    return nullptr;
+  }
+  nsCOMPtr<nsIDocShellTreeItem> rootTreeItem;
+  docShell->GetSameTypeRootTreeItem(getter_AddRefs(rootTreeItem));
+  if (!rootTreeItem || !rootTreeItem->GetDocument()) {
+    return nullptr;
+  }
+  return rootTreeItem->GetDocument()->GetInnerWindow();
+}
+
+already_AddRefed<mozilla::AutoplayRequest>
+nsPIDOMWindowInner::GetAutoplayRequest()
+{
+  // The AutoplayRequest is stored on the top level window.
+  nsPIDOMWindowInner* window = GetTopLevelInnerWindow(this);
+  if (!window) {
+    return nullptr;
+  }
+  if (!window->mAutoplayRequest) {
+    window->mAutoplayRequest = AutoplayRequest::Create(nsGlobalWindowInner::Cast(window));
+  }
+  RefPtr<mozilla::AutoplayRequest> request = window->mAutoplayRequest;
+  return request.forget();
+}
+
 // XXX: Can we define this in a header instead of here?
 namespace mozilla {
 namespace dom {
 extern uint64_t
 NextWindowID();
 } // namespace dom
 } // namespace mozilla
 
--- a/dom/base/nsPIDOMWindow.h
+++ b/dom/base/nsPIDOMWindow.h
@@ -40,16 +40,17 @@ class nsPIDOMWindowInner;
 class nsPIDOMWindowOuter;
 class nsPIWindowRoot;
 class nsXBLPrototypeHandler;
 
 typedef uint32_t SuspendTypes;
 
 namespace mozilla {
 class ThrottledEventQueue;
+class AutoplayRequest;
 namespace dom {
 class AudioContext;
 class ClientInfo;
 class ClientState;
 class DocGroup;
 class TabGroup;
 class Element;
 class Navigator;
@@ -608,16 +609,20 @@ public:
 
   virtual nsresult Focus() = 0;
   virtual nsresult Close() = 0;
 
   mozilla::dom::DocGroup* GetDocGroup() const;
   virtual nsISerialEventTarget*
   EventTargetFor(mozilla::TaskCategory aCategory) const = 0;
 
+  // Returns the AutoplayRequest that documents in this window should use
+  // to request permission to autoplay.
+  already_AddRefed<mozilla::AutoplayRequest> GetAutoplayRequest();
+
 protected:
   void CreatePerformanceObjectIfNeeded();
 
   // Lazily instantiate an about:blank document if necessary, and if
   // we have what it takes to do so.
   void MaybeCreateDoc();
 
   void SetChromeEventHandlerInternal(mozilla::dom::EventTarget* aChromeEventHandler) {
@@ -690,16 +695,21 @@ protected:
   // could be null and we don't want it to be set multiple times.
   bool mHasTriedToCacheTopInnerWindow;
 
   // The number of active IndexedDB databases.
   uint32_t mNumOfIndexedDBDatabases;
 
   // The number of open WebSockets.
   uint32_t mNumOfOpenWebSockets;
+
+  // If we're in the process of requesting permission for this window to
+  // play audible media, or we've already been granted permission by the
+  // user, this is non-null, and encapsulates the request.
+  RefPtr<mozilla::AutoplayRequest> mAutoplayRequest;
 };
 
 NS_DEFINE_STATIC_IID_ACCESSOR(nsPIDOMWindowInner, NS_PIDOMWINDOWINNER_IID)
 
 class nsPIDOMWindowOuter : public mozIDOMWindowProxy
 {
 protected:
   explicit nsPIDOMWindowOuter();
new file mode 100644
--- /dev/null
+++ b/dom/html/AutoplayRequest.cpp
@@ -0,0 +1,167 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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/dom/AutoplayRequest.h"
+
+#include "nsGlobalWindowInner.h"
+#include "nsISupportsImpl.h"
+#include "mozilla/Logging.h"
+#include "nsContentPermissionHelper.h"
+
+extern mozilla::LazyLogModule gMediaElementLog;
+
+#define PLAY_REQUEST_LOG(msg, ...)                                             \
+  MOZ_LOG(gMediaElementLog, LogLevel::Debug, (msg, ##__VA_ARGS__))
+
+namespace mozilla {
+
+NS_IMPL_ISUPPORTS(AutoplayRequest, nsIContentPermissionRequest)
+
+AutoplayRequest::AutoplayRequest(nsGlobalWindowInner* aWindow,
+                                 nsIPrincipal* aNodePrincipal,
+                                 nsIEventTarget* aMainThreadTarget)
+  : mWindow(do_GetWeakReference(aWindow))
+  , mNodePrincipal(aNodePrincipal)
+  , mMainThreadTarget(aMainThreadTarget)
+  , mRequester(new dom::nsContentPermissionRequester(aWindow))
+{
+  MOZ_RELEASE_ASSERT(mNodePrincipal);
+}
+
+AutoplayRequest::~AutoplayRequest() {}
+
+already_AddRefed<AutoplayRequest>
+AutoplayRequest::Create(nsGlobalWindowInner* aWindow)
+{
+  if (!aWindow || !aWindow->GetPrincipal() ||
+      !aWindow->EventTargetFor(TaskCategory::Other)) {
+    return nullptr;
+  }
+  RefPtr<AutoplayRequest> request =
+    new AutoplayRequest(aWindow,
+                        aWindow->GetPrincipal(),
+                        aWindow->EventTargetFor(TaskCategory::Other));
+  PLAY_REQUEST_LOG("AutoplayRequest %p Create()", request.get());
+  return request.forget();
+}
+
+NS_IMETHODIMP
+AutoplayRequest::GetPrincipal(nsIPrincipal** aRequestingPrincipal)
+{
+  NS_ENSURE_ARG_POINTER(aRequestingPrincipal);
+
+  nsCOMPtr<nsIPrincipal> principal = mNodePrincipal;
+  principal.forget(aRequestingPrincipal);
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+AutoplayRequest::GetTypes(nsIArray** aTypes)
+{
+  NS_ENSURE_ARG_POINTER(aTypes);
+
+  nsTArray<nsString> emptyOptions;
+  return dom::nsContentPermissionUtils::CreatePermissionArray(
+    NS_LITERAL_CSTRING("autoplay-media"),
+    NS_LITERAL_CSTRING("unused"),
+    emptyOptions,
+    aTypes);
+}
+
+NS_IMETHODIMP
+AutoplayRequest::GetWindow(mozIDOMWindow** aRequestingWindow)
+{
+  NS_ENSURE_ARG_POINTER(aRequestingWindow);
+
+  nsCOMPtr<nsPIDOMWindowInner> window = do_QueryReferent(mWindow);
+  if (!window) {
+    return NS_ERROR_FAILURE;
+  }
+  window.forget(aRequestingWindow);
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+AutoplayRequest::GetElement(dom::Element** aRequestingElement)
+{
+  NS_ENSURE_ARG_POINTER(aRequestingElement);
+  *aRequestingElement = nullptr;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+AutoplayRequest::GetIsHandlingUserInput(bool* aIsHandlingUserInput)
+{
+  NS_ENSURE_ARG_POINTER(aIsHandlingUserInput);
+  *aIsHandlingUserInput = false;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+AutoplayRequest::Cancel()
+{
+  MOZ_ASSERT(mRequestDispatched);
+  PLAY_REQUEST_LOG("AutoplayRequest %p Cancel()", this);
+  mRequestDispatched = false;
+  mPromiseHolder.RejectIfExists(NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR, __func__);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+AutoplayRequest::Allow(JS::HandleValue aChoices)
+{
+  MOZ_ASSERT(mRequestDispatched);
+  PLAY_REQUEST_LOG("AutoplayRequest %p Allow()", this);
+  mRequestDispatched = false;
+  mPromiseHolder.ResolveIfExists(true, __func__);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+AutoplayRequest::GetRequester(nsIContentPermissionRequester** aRequester)
+{
+  NS_ENSURE_ARG_POINTER(aRequester);
+
+  nsCOMPtr<nsIContentPermissionRequester> requester = mRequester;
+  requester.forget(aRequester);
+
+  return NS_OK;
+}
+
+RefPtr<GenericPromise>
+AutoplayRequest::RequestWithPrompt()
+{
+  // If we've already requested permission, we'll just return the promise,
+  // as we don't want to show multiple permission requests at once.
+  // The promise is non-exclusive, so if the request has already completed,
+  // the ThenValue will run immediately.
+  if (mRequestDispatched) {
+    PLAY_REQUEST_LOG(
+      "AutoplayRequest %p RequestWithPrompt() request already dispatched",
+      this);
+    return mPromiseHolder.Ensure(__func__);
+  }
+
+  nsCOMPtr<nsPIDOMWindowInner> window = do_QueryReferent(mWindow);
+  if (!window) {
+    return GenericPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR,
+                                           __func__);
+  }
+  nsCOMPtr<nsIContentPermissionRequest> request = do_QueryInterface(this);
+  MOZ_RELEASE_ASSERT(request);
+  nsCOMPtr<nsIRunnable> f = NS_NewRunnableFunction(
+    "AutoplayRequest::RequestWithPrompt", [window, request]() {
+      dom::nsContentPermissionUtils::AskPermission(request, window);
+    });
+  mMainThreadTarget->Dispatch(f, NS_DISPATCH_NORMAL);
+
+  mRequestDispatched = true;
+  return mPromiseHolder.Ensure(__func__);
+}
+
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/html/AutoplayRequest.h
@@ -0,0 +1,62 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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 __AutoplayRequest_h__
+#define __AutoplayRequest_h__
+
+#include "mozilla/MozPromise.h"
+#include "nsIContentPermissionPrompt.h"
+#include "nsIWeakReferenceUtils.h"
+
+class nsGlobalWindowInner;
+class nsIEventTarget;
+
+namespace mozilla {
+
+// Encapsulates requesting permission from the user to autoplay with a
+// doorhanger. The AutoplayRequest is stored on the top level window,
+// and all documents in the tab use the top level window's AutoplayRequest.
+// The AutoplayRequest ensures that multiple requests are merged into one,
+// in order to avoid showing multiple doorhangers on one tab at once.
+class AutoplayRequest final : public nsIContentPermissionRequest
+{
+public:
+  NS_DECL_ISUPPORTS
+  NS_DECL_NSICONTENTPERMISSIONREQUEST
+
+  // Creates a new AutoplayRequest. Don't call this directly, use
+  // AutoplayPolicy::RequestFor() to retrieve the appropriate AutoplayRequest
+  // to use for a document/window.
+  static already_AddRefed<AutoplayRequest> Create(nsGlobalWindowInner* aWindow);
+
+  // Requests permission to autoplay via a user prompt. Promise
+  // resolves/rejects when the user grants/denies permission to autoplay.
+  // If there is a stored permission for this window's origin, the stored
+  // the parent process will resolve/reject the autoplay request using the
+  // stored permission immediately (so the promise rejects in the content
+  // process after an IPC round trip).
+  RefPtr<GenericPromise> RequestWithPrompt();
+
+private:
+  AutoplayRequest(nsGlobalWindowInner* aWindow,
+                  nsIPrincipal* aNodePrincipal,
+                  nsIEventTarget* aMainThreadTarget);
+  ~AutoplayRequest();
+
+  nsWeakPtr mWindow;
+  nsCOMPtr<nsIPrincipal> mNodePrincipal;
+  nsCOMPtr<nsIEventTarget> mMainThreadTarget;
+  nsCOMPtr<nsIContentPermissionRequester> mRequester;
+  MozPromiseHolder<GenericPromise> mPromiseHolder;
+  // Tracks whether we've dispatched a request to chrome in the parent process
+  // to prompt for user permission to autoplay. This flag ensures we don't
+  // request multiple times concurrently.
+  bool mRequestDispatched = false;
+};
+
+} // namespace mozilla
+
+#endif // __AutoplayRequest_h__
--- a/dom/html/moz.build
+++ b/dom/html/moz.build
@@ -47,16 +47,17 @@ EXPORTS += [
     'nsTextEditorState.h',
 ]
 
 EXPORTS.mozilla += [
     'TextInputListener.h',
 ]
 
 EXPORTS.mozilla.dom += [
+    'AutoplayRequest.h',
     'HTMLAllCollection.h',
     'HTMLAnchorElement.h',
     'HTMLAreaElement.h',
     'HTMLAudioElement.h',
     'HTMLBodyElement.h',
     'HTMLBRElement.h',
     'HTMLButtonElement.h',
     'HTMLCanvasElement.h',
@@ -217,16 +218,17 @@ UNIFIED_SOURCES += [
     'RadioNodeList.cpp',
     'TextTrackManager.cpp',
     'TimeRanges.cpp',
     'ValidityState.cpp',
     'VideoDocument.cpp',
 ]
 
 SOURCES += [
+    'AutoplayRequest.cpp',
     # Includes npapi.h.
     'PluginDocument.cpp',
 ]
 
 EXTRA_COMPONENTS += [
     'htmlMenuBuilder.js',
     'htmlMenuBuilder.manifest'
 ]
--- a/dom/media/AutoplayPolicy.cpp
+++ b/dom/media/AutoplayPolicy.cpp
@@ -4,63 +4,116 @@
  * 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 "AutoplayPolicy.h"
 
 #include "mozilla/EventStateManager.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/dom/AudioContext.h"
+#include "mozilla/dom/AutoplayRequest.h"
 #include "mozilla/dom/HTMLMediaElement.h"
 #include "mozilla/dom/HTMLMediaElementBinding.h"
 #include "nsContentUtils.h"
 #include "nsIDocument.h"
 #include "MediaManager.h"
+#include "nsIDocShell.h"
+#include "nsIDocShellTreeItem.h"
+#include "nsPIDOMWindow.h"
 
 namespace mozilla {
 namespace dom {
 
+static nsIDocument*
+ApproverDocOf(const nsIDocument& aDocument)
+{
+  nsCOMPtr<nsIDocShell> ds = aDocument.GetDocShell();
+  if (!ds) {
+    return nullptr;
+  }
+
+  nsCOMPtr<nsIDocShellTreeItem> rootTreeItem;
+  ds->GetSameTypeRootTreeItem(getter_AddRefs(rootTreeItem));
+  if (!rootTreeItem) {
+    return nullptr;
+  }
+
+  return rootTreeItem->GetDocument();
+}
+
+static bool
+IsAllowedToPlay(nsPIDOMWindowInner* aWindow)
+{
+  if (!aWindow) {
+    return false;
+  }
+
+  // Pages which have been granted permission to capture WebRTC camera or
+  // microphone are assumed to be trusted, and are allowed to autoplay.
+  MediaManager* manager = MediaManager::GetIfExists();
+  if (manager &&
+      manager->IsActivelyCapturingOrHasAPermission(aWindow->WindowID())) {
+    return true;
+  }
+
+  if (!aWindow->GetExtantDoc()) {
+    return false;
+  }
+
+  nsIDocument* approver = ApproverDocOf(*aWindow->GetExtantDoc());
+  if (nsContentUtils::IsExactSitePermAllow(approver->NodePrincipal(),
+                                           "autoplay-media")) {
+    // Autoplay permission has been granted already.
+    return true;
+  }
+
+  if (approver->HasBeenUserGestureActivated()) {
+    // Document has been activated by user gesture.
+    return true;
+  }
+
+  return false;
+}
+
+/* static */
+already_AddRefed<AutoplayRequest>
+AutoplayPolicy::RequestFor(const nsIDocument& aDocument)
+{
+  nsIDocument* document = ApproverDocOf(aDocument);
+  if (!document) {
+    return nullptr;
+  }
+  nsPIDOMWindowInner* window = document->GetInnerWindow();
+  if (!window) {
+    return nullptr;
+  }
+  return window->GetAutoplayRequest();
+}
+
 /* static */ bool
 AutoplayPolicy::IsMediaElementAllowedToPlay(NotNull<HTMLMediaElement*> aElement)
 {
   if (Preferences::GetBool("media.autoplay.enabled")) {
     return true;
   }
 
   // TODO : this old way would be removed when user-gestures-needed becomes
   // as a default option to block autoplay.
   if (!Preferences::GetBool("media.autoplay.enabled.user-gestures-needed", false)) {
-    // If elelement is blessed, it would always be allowed to play().
+    // If element is blessed, it would always be allowed to play().
     return aElement->IsBlessed() ||
            EventStateManager::IsHandlingUserInput();
   }
 
-  // Pages which have been granted permission to capture WebRTC camera or
-  // microphone are assumed to be trusted, and are allowed to autoplay.
-  MediaManager* manager = MediaManager::GetIfExists();
-  if (manager) {
-    nsCOMPtr<nsPIDOMWindowInner> window = aElement->OwnerDoc()->GetInnerWindow();
-    if (window && manager->IsActivelyCapturingOrHasAPermission(window->WindowID())) {
-      return true;
-    }
-  }
-
   // Muted content
   if (aElement->Volume() == 0.0 || aElement->Muted()) {
     return true;
   }
 
-  // Whitelisted.
-  if (nsContentUtils::IsExactSitePermAllow(
-        aElement->NodePrincipal(), "autoplay-media")) {
-    return true;
-  }
-
-  // Activated by user gesture.
-  if (aElement->OwnerDoc()->HasBeenUserGestureActivated()) {
+  if (IsAllowedToPlay(aElement->OwnerDoc()->GetInnerWindow())) {
     return true;
   }
 
   return false;
 }
 
 /* static */ bool
 AutoplayPolicy::IsAudioContextAllowedToPlay(NotNull<AudioContext*> aContext)
@@ -73,40 +126,17 @@ AutoplayPolicy::IsAudioContextAllowedToP
     return true;
   }
 
   // Offline context won't directly output sound to audio devices.
   if (aContext->IsOffline()) {
     return true;
   }
 
-  nsPIDOMWindowInner* window = aContext->GetOwner();
-  if (!window) {
-    return false;
-  }
-
-  // Pages which have been granted permission to capture WebRTC camera or
-  // microphone are assumed to be trusted, and are allowed to autoplay.
-  MediaManager* manager = MediaManager::GetIfExists();
-  if (manager) {
-    if (manager->IsActivelyCapturingOrHasAPermission(window->WindowID())) {
-      return true;
-    }
-  }
-
-  nsCOMPtr<nsIPrincipal> principal = aContext->GetParentObject()->AsGlobal()->PrincipalOrNull();
-
-  // Whitelisted.
-  if (principal &&
-      nsContentUtils::IsExactSitePermAllow(principal, "autoplay-media")) {
-    return true;
-  }
-
-  // Activated by user gesture.
-  if (window->GetExtantDoc()->HasBeenUserGestureActivated()) {
+  if (IsAllowedToPlay(aContext->GetOwner())) {
     return true;
   }
 
   return false;
 }
 
 } // namespace dom
 } // namespace mozilla
--- a/dom/media/AutoplayPolicy.h
+++ b/dom/media/AutoplayPolicy.h
@@ -7,16 +7,19 @@
 #if !defined(AutoplayPolicy_h_)
 #define AutoplayPolicy_h_
 
 #include "mozilla/NotNull.h"
 
 class nsIDocument;
 
 namespace mozilla {
+
+class AutoplayRequest;
+
 namespace dom {
 
 class HTMLMediaElement;
 class AudioContext;
 
 /**
  * AutoplayPolicy is used to manage autoplay logic for all kinds of media,
  * including MediaElement, Web Audio and Web Speech.
@@ -27,18 +30,24 @@ class AudioContext;
  * 1) Owner document is activated by user gestures
  *    We restrict user gestures to "mouse click", "keyboard press" and "touch".
  * 2) Muted media content or video without audio content.
  * 3) Document's origin has the "autoplay-media" permission.
  */
 class AutoplayPolicy
 {
 public:
+  // Returns whether a given media element is allowed to play.
   static bool IsMediaElementAllowedToPlay(NotNull<HTMLMediaElement*> aElement);
+
+  // Returns whether a given AudioContext is allowed to play.
   static bool IsAudioContextAllowedToPlay(NotNull<AudioContext*> aContext);
-private:
-  static bool IsDocumentAllowedToPlay(nsIDocument* aDoc);
+
+  // Returns the AutoplayRequest that a given document must request on
+  // for autoplay permission.
+  static already_AddRefed<AutoplayRequest> RequestFor(
+    const nsIDocument& aDocument);
 };
 
 } // namespace dom
 } // namespace mozilla
 
 #endif