/* -*- 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/EventStateManager.h" #include "mozilla/Logging.h" #include "mozilla/Preferences.h" #include "mozilla/dom/AudioContext.h" #include "mozilla/dom/FeaturePolicyUtils.h" #include "mozilla/dom/HTMLMediaElement.h" #include "mozilla/dom/HTMLMediaElementBinding.h" #include "nsGlobalWindowInner.h" #include "nsIAutoplay.h" #include "nsContentUtils.h" #include "mozilla/dom/Document.h" #include "MediaManager.h" #include "nsIDocShell.h" #include "nsIDocShellTreeItem.h" #include "nsPIDOMWindow.h" mozilla::LazyLogModule gAutoplayPermissionLog("Autoplay"); #define AUTOPLAY_LOG(msg, ...) \ MOZ_LOG(gAutoplayPermissionLog, LogLevel::Debug, (msg, ##__VA_ARGS__)) namespace mozilla { namespace dom { static Document* ApproverDocOf(const Document& aDocument) { nsCOMPtr ds = aDocument.GetDocShell(); if (!ds) { return nullptr; } nsCOMPtr rootTreeItem; ds->GetSameTypeRootTreeItem(getter_AddRefs(rootTreeItem)); if (!rootTreeItem) { return nullptr; } return rootTreeItem->GetDocument(); } 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, NS_LITERAL_CSTRING("camera")) || nsContentUtils::IsExactSitePermAllow( principal, NS_LITERAL_CSTRING("microphone")) || nsContentUtils::IsExactSitePermAllow(principal, NS_LITERAL_CSTRING("screen"))); } static bool IsSiteInAutoplayWhiteList(const Document* aDocument) { return aDocument ? nsContentUtils::IsExactSitePermAllow( aDocument->NodePrincipal(), NS_LITERAL_CSTRING("autoplay-media")) : false; } static bool IsSiteInAutoplayBlackList(const Document* aDocument) { return aDocument ? nsContentUtils::IsExactSitePermDeny( aDocument->NodePrincipal(), NS_LITERAL_CSTRING("autoplay-media")) : false; } static bool IsWindowAllowedToPlay(nsPIDOMWindowInner* aWindow) { if (!aWindow) { return false; } if (IsActivelyCapturingOrHasAPermission(aWindow)) { AUTOPLAY_LOG( "Allow autoplay as document has camera or microphone or screen" " permission."); return true; } if (!aWindow->GetExtantDoc()) { return false; } Document* approver = ApproverDocOf(*aWindow->GetExtantDoc()); if (!approver) { return false; } if (approver->HasBeenUserGestureActivated()) { AUTOPLAY_LOG("Allow autoplay as document activated by user gesture."); return true; } if (approver->IsExtensionPage()) { AUTOPLAY_LOG("Allow autoplay as in extension document."); return true; } if (approver->MediaDocumentKind() == Document::MediaDocumentKind::Video) { AUTOPLAY_LOG("Allow video document to autoplay."); return true; } return false; } static uint32_t DefaultAutoplayBehaviour() { int prefValue = Preferences::GetInt("media.autoplay.default", nsIAutoplay::ALLOWED); if (prefValue < nsIAutoplay::ALLOWED || prefValue > nsIAutoplay::BLOCKED) { // Invalid pref values are just converted to BLOCKED. return nsIAutoplay::BLOCKED; } return prefValue; } static bool IsMediaElementAllowedToPlay(const HTMLMediaElement& aElement) { const bool isAllowedMuted = Preferences::GetBool("media.autoplay.allow-muted", true); if ((aElement.Volume() == 0.0 || aElement.Muted()) && isAllowedMuted) { AUTOPLAY_LOG("Allow muted media %p to autoplay.", &aElement); return true; } if (!aElement.HasAudio() && aElement.ReadyState() >= HTMLMediaElement_Binding::HAVE_METADATA && isAllowedMuted) { AUTOPLAY_LOG("Allow media %p without audio track to autoplay", &aElement); return true; } return false; } static bool IsAudioContextAllowedToPlay(const AudioContext& aContext) { // Offline context won't directly output sound to audio devices. return aContext.IsOffline() || IsWindowAllowedToPlay(aContext.GetParentObject()); } static bool IsEnableBlockingWebAudioByUserGesturePolicy() { return DefaultAutoplayBehaviour() != nsIAutoplay::ALLOWED && Preferences::GetBool("media.autoplay.block-webaudio", false) && Preferences::GetBool("media.autoplay.enabled.user-gestures-needed", false); } /* static */ bool AutoplayPolicy::WouldBeAllowedToPlayIfAutoplayDisabled( const HTMLMediaElement& aElement) { return IsMediaElementAllowedToPlay(aElement) || IsWindowAllowedToPlay(aElement.OwnerDoc()->GetInnerWindow()); } /* static */ bool AutoplayPolicy::WouldBeAllowedToPlayIfAutoplayDisabled( const AudioContext& aContext) { return IsAudioContextAllowedToPlay(aContext); } static bool IsAllowedToPlayInternal(const HTMLMediaElement& aElement) { /** * The autoplay checking has 4 different phases, * 1. check whether media element itself meets the autoplay condition * 2. check whethr the site is in the autoplay whitelist * 3. check global autoplay setting and check wether the site is in the * autoplay blacklist. * 4. check whether media is allowed under current blocking model * (click-to-play or user-gesture-activation) */ if (IsMediaElementAllowedToPlay(aElement)) { return true; } Document* approver = ApproverDocOf(*aElement.OwnerDoc()); if (IsSiteInAutoplayWhiteList(approver)) { AUTOPLAY_LOG( "Allow autoplay as document has permanent autoplay permission."); return true; } if (DefaultAutoplayBehaviour() == nsIAutoplay::ALLOWED && !(IsSiteInAutoplayBlackList(approver) && StaticPrefs::MediaAutoplayBlackListOverrideDefault())) { AUTOPLAY_LOG( "Allow autoplay as global autoplay setting is allowing autoplay by " "default."); return true; } if (!Preferences::GetBool("media.autoplay.enabled.user-gestures-needed", false)) { // If element is blessed, it would always be allowed to play(). return aElement.IsBlessed() || EventStateManager::IsHandlingUserInput(); } return IsWindowAllowedToPlay(aElement.OwnerDoc()->GetInnerWindow()); } /* 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 4 different phases, * 1. check whether audio context itself meets the autoplay condition * 2. check whethr the site is in the autoplay whitelist * 3. check global autoplay setting and check wether the site is in the * autoplay blacklist. * 4. check whether media is allowed under current blocking model * (only support user-gesture-activation) */ if (aContext.IsOffline()) { return true; } nsPIDOMWindowInner* window = aContext.GetParentObject(); Document* approver = aContext.GetParentObject() ? ApproverDocOf(*(window->GetExtantDoc())) : nullptr; if (IsSiteInAutoplayWhiteList(approver)) { AUTOPLAY_LOG( "Allow autoplay as document has permanent autoplay permission."); return true; } if (DefaultAutoplayBehaviour() == nsIAutoplay::ALLOWED && !IsSiteInAutoplayBlackList(approver)) { AUTOPLAY_LOG( "Allow autoplay as global autoplay setting is allowing autoplay by " "default."); return true; } if (!IsEnableBlockingWebAudioByUserGesturePolicy()) { return true; } return IsWindowAllowedToPlay(window); } /* static */ DocumentAutoplayPolicy AutoplayPolicy::IsAllowedToPlay( const Document& aDocument) { if (DefaultAutoplayBehaviour() == nsIAutoplay::ALLOWED || IsWindowAllowedToPlay(aDocument.GetInnerWindow())) { return DocumentAutoplayPolicy::Allowed; } if (StaticPrefs::MediaAutoplayAllowMuted()) { return DocumentAutoplayPolicy::Allowed_muted; } return DocumentAutoplayPolicy::Disallowed; } } // namespace dom } // namespace mozilla