/* -*- 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
