diff options
Diffstat (limited to 'dom/media/autoplay/AutoplayPolicy.cpp')
-rw-r--r-- | dom/media/autoplay/AutoplayPolicy.cpp | 497 |
1 files changed, 497 insertions, 0 deletions
diff --git a/dom/media/autoplay/AutoplayPolicy.cpp b/dom/media/autoplay/AutoplayPolicy.cpp new file mode 100644 index 0000000000..8da0e60ead --- /dev/null +++ b/dom/media/autoplay/AutoplayPolicy.cpp @@ -0,0 +1,497 @@ +/* -*- 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 "AutoplayPolicy.h" + +#include "mozilla/dom/AudioContext.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/FeaturePolicyUtils.h" +#include "mozilla/dom/HTMLMediaElement.h" +#include "mozilla/dom/HTMLMediaElementBinding.h" +#include "mozilla/dom/NavigatorBinding.h" +#include "mozilla/dom/UserActivation.h" +#include "mozilla/dom/WindowContext.h" +#include "mozilla/Logging.h" +#include "mozilla/MediaManager.h" +#include "mozilla/Components.h" +#include "mozilla/StaticPrefs_media.h" +#include "nsContentUtils.h" +#include "nsGlobalWindowInner.h" +#include "nsIAutoplay.h" +#include "nsIDocShell.h" +#include "nsIDocShellTreeItem.h" +#include "nsIPermissionManager.h" +#include "nsIPrincipal.h" +#include "nsPIDOMWindow.h" + +mozilla::LazyLogModule gAutoplayPermissionLog("Autoplay"); + +#define AUTOPLAY_LOG(msg, ...) \ + MOZ_LOG(gAutoplayPermissionLog, LogLevel::Debug, (msg, ##__VA_ARGS__)) + +using namespace mozilla::dom; + +namespace mozilla::media { + +static const uint32_t sPOLICY_STICKY_ACTIVATION = 0; +// static const uint32_t sPOLICY_TRANSIENT_ACTIVATION = 1; +static const uint32_t sPOLICY_USER_INPUT_DEPTH = 2; + +static bool IsActivelyCapturingOrHasAPermission(nsPIDOMWindowInner* aWindow) { + // Pages which have been granted permission to capture WebRTC camera or + // microphone or screen are assumed to be trusted, and are allowed to + // autoplay. + if (MediaManager::GetIfExists()) { + return MediaManager::GetIfExists()->IsActivelyCapturingOrHasAPermission( + aWindow->WindowID()); + } + + auto principal = nsGlobalWindowInner::Cast(aWindow)->GetPrincipal(); + return (nsContentUtils::IsExactSitePermAllow(principal, "camera"_ns) || + nsContentUtils::IsExactSitePermAllow(principal, "microphone"_ns) || + nsContentUtils::IsExactSitePermAllow(principal, "screen"_ns)); +} + +static uint32_t SiteAutoplayPerm(nsPIDOMWindowInner* aWindow) { + if (!aWindow || !aWindow->GetBrowsingContext()) { + return nsIPermissionManager::UNKNOWN_ACTION; + } + + WindowContext* topContext = + aWindow->GetBrowsingContext()->GetTopWindowContext(); + if (!topContext) { + return nsIPermissionManager::UNKNOWN_ACTION; + } + return topContext->GetAutoplayPermission(); +} + +static bool IsWindowAllowedToPlayByUserGesture(nsPIDOMWindowInner* aWindow) { + if (!aWindow) { + return false; + } + + WindowContext* topContext = + aWindow->GetBrowsingContext()->GetTopWindowContext(); + if (topContext && topContext->HasBeenUserGestureActivated()) { + AUTOPLAY_LOG( + "Allow autoplay as top-level context has been activated by user " + "gesture."); + return true; + } + return false; +} + +static bool IsWindowAllowedToPlayByTraits(nsPIDOMWindowInner* aWindow) { + if (!aWindow) { + return false; + } + + if (IsActivelyCapturingOrHasAPermission(aWindow)) { + AUTOPLAY_LOG( + "Allow autoplay as document has camera or microphone or screen" + " permission."); + return true; + } + + Document* currentDoc = aWindow->GetExtantDoc(); + if (!currentDoc) { + return false; + } + + bool isTopLevelContent = !aWindow->GetBrowsingContext()->GetParent(); + if (currentDoc->MediaDocumentKind() == Document::MediaDocumentKind::Video && + isTopLevelContent) { + AUTOPLAY_LOG("Allow top-level video document to autoplay."); + return true; + } + + if (StaticPrefs::media_autoplay_allow_extension_background_pages() && + currentDoc->IsExtensionPage()) { + AUTOPLAY_LOG("Allow autoplay as in extension document."); + return true; + } + + return false; +} + +static bool IsWindowAllowedToPlayOverall(nsPIDOMWindowInner* aWindow) { + return IsWindowAllowedToPlayByUserGesture(aWindow) || + IsWindowAllowedToPlayByTraits(aWindow); +} + +static uint32_t DefaultAutoplayBehaviour() { + int32_t prefValue = StaticPrefs::media_autoplay_default(); + if (prefValue == nsIAutoplay::ALLOWED) { + return nsIAutoplay::ALLOWED; + } + if (prefValue == nsIAutoplay::BLOCKED_ALL) { + return nsIAutoplay::BLOCKED_ALL; + } + return nsIAutoplay::BLOCKED; +} + +static bool IsMediaElementInaudible(const HTMLMediaElement& aElement) { + if (aElement.Volume() == 0.0 || aElement.Muted()) { + AUTOPLAY_LOG("Media %p is muted.", &aElement); + return true; + } + + if (!aElement.HasAudio() && + aElement.ReadyState() >= HTMLMediaElement_Binding::HAVE_METADATA) { + AUTOPLAY_LOG("Media %p has no audio track", &aElement); + return true; + } + + return false; +} + +static bool IsAudioContextAllowedToPlay(const AudioContext& aContext) { + // Offline context won't directly output sound to audio devices. + return aContext.IsOffline() || + IsWindowAllowedToPlayOverall(aContext.GetParentObject()); +} + +static bool IsEnableBlockingWebAudioByUserGesturePolicy() { + return StaticPrefs::media_autoplay_block_webaudio() && + StaticPrefs::media_autoplay_blocking_policy() == + sPOLICY_STICKY_ACTIVATION; +} + +static bool IsAllowedToPlayByBlockingModel(const HTMLMediaElement& aElement) { + const uint32_t policy = StaticPrefs::media_autoplay_blocking_policy(); + if (policy == sPOLICY_STICKY_ACTIVATION) { + const bool isAllowed = + IsWindowAllowedToPlayOverall(aElement.OwnerDoc()->GetInnerWindow()); + AUTOPLAY_LOG("Use 'sticky-activation', isAllowed=%d", isAllowed); + return isAllowed; + } + // If element is blessed, it would always be allowed to play(). + const bool isElementBlessed = aElement.IsBlessed(); + if (policy == sPOLICY_USER_INPUT_DEPTH) { + const bool isUserInput = UserActivation::IsHandlingUserInput(); + AUTOPLAY_LOG("Use 'User-Input-Depth', isBlessed=%d, isUserInput=%d", + isElementBlessed, isUserInput); + return isElementBlessed || isUserInput; + } + const bool hasTransientActivation = + aElement.OwnerDoc()->HasValidTransientUserGestureActivation(); + AUTOPLAY_LOG( + "Use 'transient-activation', isBlessed=%d, " + "hasValidTransientActivation=%d", + isElementBlessed, hasTransientActivation); + return isElementBlessed || hasTransientActivation; +} + +// On GeckoView, we don't store any site's permission in permission manager, we +// would check the GV request status to know if the site can be allowed to play. +// But on other platforms, we would store the site's permission in permission +// manager. +#if defined(MOZ_WIDGET_ANDROID) +using RType = GVAutoplayRequestType; + +static bool IsGVAutoplayRequestAllowed(nsPIDOMWindowInner* aWindow, + RType aType) { + if (!aWindow) { + return false; + } + + RefPtr<BrowsingContext> context = aWindow->GetBrowsingContext()->Top(); + GVAutoplayRequestStatus status = + aType == RType::eAUDIBLE ? context->GetGVAudibleAutoplayRequestStatus() + : context->GetGVInaudibleAutoplayRequestStatus(); + return status == GVAutoplayRequestStatus::eALLOWED; +} + +static bool IsGVAutoplayRequestAllowed(const HTMLMediaElement& aElement, + RType aType) { + // On GV, blocking model is the first thing we would check inside Gecko, and + // if the media is not allowed by that, then we would check the response from + // the embedding app to decide the final result. + if (IsAllowedToPlayByBlockingModel(aElement)) { + return true; + } + + RefPtr<nsPIDOMWindowInner> window = aElement.OwnerDoc()->GetInnerWindow(); + if (!window) { + return false; + } + return IsGVAutoplayRequestAllowed(window, aType); +} +#endif + +static bool IsAllowedToPlayInternal(const HTMLMediaElement& aElement) { +#if defined(MOZ_WIDGET_ANDROID) + if (StaticPrefs::media_geckoview_autoplay_request()) { + return IsGVAutoplayRequestAllowed( + aElement, IsMediaElementInaudible(aElement) ? RType::eINAUDIBLE + : RType::eAUDIBLE); + } +#endif + bool isInaudible = IsMediaElementInaudible(aElement); + bool isUsingAutoplayModel = IsAllowedToPlayByBlockingModel(aElement); + + uint32_t defaultBehaviour = DefaultAutoplayBehaviour(); + uint32_t sitePermission = + SiteAutoplayPerm(aElement.OwnerDoc()->GetInnerWindow()); + + AUTOPLAY_LOG( + "IsAllowedToPlayInternal, isInaudible=%d," + "isUsingAutoplayModel=%d, sitePermission=%d, defaultBehaviour=%d", + isInaudible, isUsingAutoplayModel, sitePermission, defaultBehaviour); + + // For site permissions we store permissionManager values except + // for BLOCKED_ALL, for the default pref values we store + // nsIAutoplay values. + if (sitePermission == nsIPermissionManager::ALLOW_ACTION) { + return true; + } + + if (sitePermission == nsIPermissionManager::DENY_ACTION) { + return isInaudible || isUsingAutoplayModel; + } + + if (sitePermission == nsIAutoplay::BLOCKED_ALL) { + return isUsingAutoplayModel; + } + + if (defaultBehaviour == nsIAutoplay::ALLOWED) { + return true; + } + + if (defaultBehaviour == nsIAutoplay::BLOCKED) { + return isInaudible || isUsingAutoplayModel; + } + + MOZ_ASSERT(defaultBehaviour == nsIAutoplay::BLOCKED_ALL); + return isUsingAutoplayModel; +} + +/* static */ +bool AutoplayPolicy::IsAllowedToPlay(const HTMLMediaElement& aElement) { + const bool result = IsAllowedToPlayInternal(aElement); + AUTOPLAY_LOG("IsAllowedToPlay, mediaElement=%p, isAllowToPlay=%s", &aElement, + result ? "allowed" : "blocked"); + return result; +} + +/* static */ +bool AutoplayPolicy::IsAllowedToPlay(const AudioContext& aContext) { + /** + * The autoplay checking has 5 different phases, + * 1. check whether audio context itself meets the autoplay condition + * 2. check if we enable blocking web audio or not + * (only support blocking when using user-gesture-activation model) + * 3. check whether the site is in the autoplay whitelist + * 4. check global autoplay setting and check wether the site is in the + * autoplay blacklist. + * 5. check whether media is allowed under current blocking model + * (only support user-gesture-activation model) + */ + if (aContext.IsOffline()) { + return true; + } + + if (!IsEnableBlockingWebAudioByUserGesturePolicy()) { + return true; + } + + nsPIDOMWindowInner* window = aContext.GetParentObject(); + uint32_t sitePermission = SiteAutoplayPerm(window); + + if (sitePermission == nsIPermissionManager::ALLOW_ACTION) { + AUTOPLAY_LOG( + "Allow autoplay as document has permanent autoplay permission."); + return true; + } + + if (DefaultAutoplayBehaviour() == nsIAutoplay::ALLOWED && + sitePermission != nsIPermissionManager::DENY_ACTION && + sitePermission != nsIAutoplay::BLOCKED_ALL) { + AUTOPLAY_LOG( + "Allow autoplay as global autoplay setting is allowing autoplay by " + "default."); + return true; + } + + return IsWindowAllowedToPlayOverall(window); +} + +enum class DocumentAutoplayPolicy : uint8_t { + Allowed, + Allowed_muted, + Disallowed +}; + +/* static */ +DocumentAutoplayPolicy IsDocAllowedToPlay(const Document& aDocument) { + RefPtr<nsPIDOMWindowInner> window = aDocument.GetInnerWindow(); + +#if defined(MOZ_WIDGET_ANDROID) + if (StaticPrefs::media_geckoview_autoplay_request()) { + const bool isWindowAllowedToPlay = IsWindowAllowedToPlayOverall(window); + if (IsGVAutoplayRequestAllowed(window, RType::eAUDIBLE)) { + return DocumentAutoplayPolicy::Allowed; + } + + if (IsGVAutoplayRequestAllowed(window, RType::eINAUDIBLE)) { + return isWindowAllowedToPlay ? DocumentAutoplayPolicy::Allowed + : DocumentAutoplayPolicy::Allowed_muted; + } + + return isWindowAllowedToPlay ? DocumentAutoplayPolicy::Allowed + : DocumentAutoplayPolicy::Disallowed; + } +#endif + const uint32_t sitePermission = SiteAutoplayPerm(window); + const uint32_t globalPermission = DefaultAutoplayBehaviour(); + const uint32_t policy = StaticPrefs::media_autoplay_blocking_policy(); + const bool isWindowAllowedToPlayByGesture = + policy != sPOLICY_USER_INPUT_DEPTH && + IsWindowAllowedToPlayByUserGesture(window); + const bool isWindowAllowedToPlayByTraits = + IsWindowAllowedToPlayByTraits(window); + + AUTOPLAY_LOG( + "IsDocAllowedToPlay(), policy=%d, sitePermission=%d, " + "globalPermission=%d, isWindowAllowedToPlayByGesture=%d, " + "isWindowAllowedToPlayByTraits=%d", + policy, sitePermission, globalPermission, isWindowAllowedToPlayByGesture, + isWindowAllowedToPlayByTraits); + + if ((globalPermission == nsIAutoplay::ALLOWED && + (sitePermission != nsIPermissionManager::DENY_ACTION && + sitePermission != nsIAutoplay::BLOCKED_ALL)) || + sitePermission == nsIPermissionManager::ALLOW_ACTION || + isWindowAllowedToPlayByGesture || isWindowAllowedToPlayByTraits) { + return DocumentAutoplayPolicy::Allowed; + } + + if ((globalPermission == nsIAutoplay::BLOCKED && + sitePermission != nsIAutoplay::BLOCKED_ALL) || + sitePermission == nsIPermissionManager::DENY_ACTION) { + return DocumentAutoplayPolicy::Allowed_muted; + } + + return DocumentAutoplayPolicy::Disallowed; +} + +/* static */ +uint32_t AutoplayPolicy::GetSiteAutoplayPermission(nsIPrincipal* aPrincipal) { + if (!aPrincipal) { + return nsIPermissionManager::DENY_ACTION; + } + + nsCOMPtr<nsIPermissionManager> permMgr = + components::PermissionManager::Service(); + if (!permMgr) { + return nsIPermissionManager::DENY_ACTION; + } + + uint32_t perm = nsIPermissionManager::DENY_ACTION; + permMgr->TestExactPermissionFromPrincipal(aPrincipal, "autoplay-media"_ns, + &perm); + return perm; +} + +/* static */ +bool AutoplayPolicyTelemetryUtils::WouldBeAllowedToPlayIfAutoplayDisabled( + const AudioContext& aContext) { + return IsAudioContextAllowedToPlay(aContext); +} + +/* static */ +dom::AutoplayPolicy AutoplayPolicy::GetAutoplayPolicy( + const dom::HTMLMediaElement& aElement) { + // Note, the site permission can contain following values : + // - UNKNOWN_ACTION : no permission set for this site + // - ALLOW_ACTION : allowed to autoplay + // - DENY_ACTION : allowed inaudible autoplay, disallowed inaudible autoplay + // - nsIAutoplay::BLOCKED_ALL : autoplay disallowed + // and the global permissions would be nsIAutoplay::{BLOCKED, ALLOWED, + // BLOCKED_ALL} + const uint32_t sitePermission = + SiteAutoplayPerm(aElement.OwnerDoc()->GetInnerWindow()); + const uint32_t globalPermission = DefaultAutoplayBehaviour(); + const bool isAllowedToPlayByBlockingModel = + IsAllowedToPlayByBlockingModel(aElement); + + AUTOPLAY_LOG( + "IsAllowedToPlay(element), sitePermission=%d, globalPermission=%d, " + "isAllowedToPlayByBlockingModel=%d", + sitePermission, globalPermission, isAllowedToPlayByBlockingModel); + +#if defined(MOZ_WIDGET_ANDROID) + if (StaticPrefs::media_geckoview_autoplay_request()) { + if (IsGVAutoplayRequestAllowed(aElement, RType::eAUDIBLE)) { + return dom::AutoplayPolicy::Allowed; + } else if (IsGVAutoplayRequestAllowed(aElement, RType::eINAUDIBLE)) { + return isAllowedToPlayByBlockingModel + ? dom::AutoplayPolicy::Allowed + : dom::AutoplayPolicy::Allowed_muted; + } else { + return isAllowedToPlayByBlockingModel ? dom::AutoplayPolicy::Allowed + : dom::AutoplayPolicy::Disallowed; + } + } +#endif + + // These are situations when an element is allowed to autoplay + // 1. The site permission is explicitly allowed + // 2. The global permission is allowed, and the site isn't explicitly + // disallowed + // 3. The blocking model is explicitly allowed this element + if (sitePermission == nsIPermissionManager::ALLOW_ACTION || + (globalPermission == nsIAutoplay::ALLOWED && + (sitePermission != nsIPermissionManager::DENY_ACTION && + sitePermission != nsIAutoplay::BLOCKED_ALL)) || + isAllowedToPlayByBlockingModel) { + return dom::AutoplayPolicy::Allowed; + } + + // These are situations when a element is allowed to autoplay only when it's + // inaudible. + // 1. The site permission is block-audible-autoplay + // 2. The global permission is block-audible-autoplay, and the site permission + // isn't block-all-autoplay + if (sitePermission == nsIPermissionManager::DENY_ACTION || + (globalPermission == nsIAutoplay::BLOCKED && + sitePermission != nsIAutoplay::BLOCKED_ALL)) { + return dom::AutoplayPolicy::Allowed_muted; + } + + return dom::AutoplayPolicy::Disallowed; +} + +/* static */ +dom::AutoplayPolicy AutoplayPolicy::GetAutoplayPolicy( + const dom::AudioContext& aContext) { + if (AutoplayPolicy::IsAllowedToPlay(aContext)) { + return dom::AutoplayPolicy::Allowed; + } + return dom::AutoplayPolicy::Disallowed; +} + +/* static */ +dom::AutoplayPolicy AutoplayPolicy::GetAutoplayPolicy( + const dom::AutoplayPolicyMediaType& aType, const dom::Document& aDoc) { + DocumentAutoplayPolicy policy = IsDocAllowedToPlay(aDoc); + // https://w3c.github.io/autoplay/#query-by-a-media-type + if (aType == dom::AutoplayPolicyMediaType::Audiocontext) { + return policy == DocumentAutoplayPolicy::Allowed + ? dom::AutoplayPolicy::Allowed + : dom::AutoplayPolicy::Disallowed; + } + MOZ_ASSERT(aType == dom::AutoplayPolicyMediaType::Mediaelement); + if (policy == DocumentAutoplayPolicy::Allowed) { + return dom::AutoplayPolicy::Allowed; + } + if (policy == DocumentAutoplayPolicy::Allowed_muted) { + return dom::AutoplayPolicy::Allowed_muted; + } + return dom::AutoplayPolicy::Disallowed; +} + +} // namespace mozilla::media |