// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/media/capture_handle_manager.h"

#include <algorithm>

#include "base/memory/ptr_util.h"
#include "build/build_config.h"
#include "content/browser/renderer_host/render_frame_host_delegate.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"
#include "url/origin.h"

#if defined(USE_AURA)
#include "ui/aura/window.h"
#endif

namespace content {

namespace {
// TODO(crbug.com/40181897): Eliminate code duplication with
// desktop_capture_devices_util.cc.
media::mojom::CaptureHandlePtr CreateCaptureHandle(
    RenderFrameHostImpl* capturer,
    WebContents* captured,
    const blink::mojom::CaptureHandleConfig& capture_handle_config) {
  if (!captured) {
    return nullptr;
  }

  if (!capture_handle_config.expose_origin &&
      capture_handle_config.capture_handle.empty()) {
    return nullptr;
  }

  const url::Origin& capturer_origin = capturer->GetLastCommittedOrigin();
  if (!capture_handle_config.all_origins_permitted &&
      std::ranges::none_of(
          capture_handle_config.permitted_origins,
          [capturer_origin](const url::Origin& permitted_origin) {
            return capturer_origin.IsSameOriginWith(permitted_origin);
          })) {
    return nullptr;
  }

  // Observing CaptureHandle wheneither the capturing or the captured party
  // is incognito is disallowed, except for self-capture.
  if (capturer->GetMainFrame() != captured->GetPrimaryMainFrame()) {
    if (capturer->GetBrowserContext()->IsOffTheRecord() ||
        captured->GetBrowserContext()->IsOffTheRecord()) {
      return nullptr;
    }
  }

  auto result = media::mojom::CaptureHandle::New();
  if (capture_handle_config.expose_origin) {
    result->origin = captured->GetPrimaryMainFrame()->GetLastCommittedOrigin();
  }
  result->capture_handle = capture_handle_config.capture_handle;

  return result;
}

bool IsEqual(const media::mojom::CaptureHandlePtr& lhs,
             const media::mojom::CaptureHandlePtr& rhs) {
  if (!lhs || !rhs) {  // If either is null, equal only if both are null.
    return !lhs && !rhs;
  }

  if (lhs->origin.opaque() != rhs->origin.opaque()) {
    return false;  // One is empty, the other is non-empty.
  }

  // Either both are opaque or neither is. We only compare non-opaque origins.
  if (!lhs->origin.opaque()) {
    if (lhs->origin != rhs->origin) {
      return false;
    }
  }

  return lhs->capture_handle == rhs->capture_handle;
}

std::optional<GlobalRenderFrameHostId> GetCapturedFrameHostId(
    const std::string& device_id) {
  WebContentsMediaCaptureId captured_tab_id;
  if (WebContentsMediaCaptureId::Parse(device_id, &captured_tab_id)) {
    return GlobalRenderFrameHostId(captured_tab_id.render_process_id,
                                   captured_tab_id.main_render_frame_id);
  }

  content::DesktopMediaID desktop_id =
      content::DesktopMediaID::Parse(device_id);

  if (desktop_id.is_null() ||
      desktop_id.type != content::DesktopMediaID::TYPE_WINDOW) {
    DVLOG(1) << "Not a tab-capture or supported window-capture ID: "
             << device_id << ".";
    return std::nullopt;
  }

#if defined(USE_AURA) || BUILDFLAG(IS_MAC)
  gfx::NativeWindow target_window =
      content::DesktopMediaID::GetNativeWindowById(desktop_id);

  WebContents* web_contents =
      GetContentClient()
          ->browser()
          ->GetWebContentsFromWindowIfCaptureHandleAllowed(target_window);

  if (!web_contents) {
    DVLOG(1) << "Window does not host a WebContents or capture handle is "
                "disallowed.";
    return std::nullopt;
  }

  return web_contents->GetPrimaryMainFrame()->GetGlobalId();
#else
  DVLOG(1)
      << "Window capture handle parsing is only supported on this platform.";
  return std::nullopt;
#endif
}
}  // namespace

class CaptureHandleManager::Observer final : public WebContentsObserver {
 public:
  static std::unique_ptr<Observer> Create(
      const CaptureKey& capture_key,
      GlobalRenderFrameHostId captured,
      GlobalRenderFrameHostId capturer,
      DeviceCaptureHandleChangeCallback handle_change_callback);

  ~Observer() override;

  // Implements WebContentsObserver.
  void OnCaptureHandleConfigUpdate(
      const blink::mojom::CaptureHandleConfig& config) override;

  // Forces an immediate polling of the captured tab for the current config.
  // Reports it back via |handle_change_callback_|.
  void UpdateCaptureHandleConfig();

 private:
  Observer(WebContents* web_contents,
           const CaptureKey& capture_key,
           GlobalRenderFrameHostId capturer,
           DeviceCaptureHandleChangeCallback handle_change_callback);

  const CaptureKey capture_key_;
  const GlobalRenderFrameHostId capturer_;
  const DeviceCaptureHandleChangeCallback handle_change_callback_;
};

std::unique_ptr<CaptureHandleManager::Observer>
CaptureHandleManager::Observer::Create(
    const CaptureKey& capture_key,
    GlobalRenderFrameHostId captured,
    GlobalRenderFrameHostId capturer,
    DeviceCaptureHandleChangeCallback handle_change_callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  auto* const capturer_rfhi = RenderFrameHostImpl::FromID(capturer);
  if (!capturer_rfhi || !capturer_rfhi->IsActive()) {
    return nullptr;
  }

  auto* const captured_rfhi = RenderFrameHostImpl::FromID(captured);
  if (!captured_rfhi || !captured_rfhi->IsActive()) {
    return nullptr;
  }

  auto* const captured_web_contents =
      WebContents::FromRenderFrameHost(captured_rfhi);
  if (!captured_web_contents) {
    return nullptr;
  }

  return base::WrapUnique(new Observer(captured_web_contents, capture_key,
                                       capturer,
                                       std::move(handle_change_callback)));
}

CaptureHandleManager::Observer::Observer(
    WebContents* web_contents,
    const CaptureKey& capture_key,
    GlobalRenderFrameHostId capturer,
    DeviceCaptureHandleChangeCallback handle_change_callback)
    : WebContentsObserver(web_contents),
      capture_key_(capture_key),
      capturer_(capturer),
      handle_change_callback_(std::move(handle_change_callback)) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK(handle_change_callback_);
}

CaptureHandleManager::Observer::~Observer() = default;

void CaptureHandleManager::Observer::OnCaptureHandleConfigUpdate(
    const blink::mojom::CaptureHandleConfig& config) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  auto* const capturer_rfhi = RenderFrameHostImpl::FromID(capturer_);
  if (!capturer_rfhi || !capturer_rfhi->IsActive()) {
    DVLOG(1) << "Invalid capturer: " << capturer_ << ".";
    return;
  }

  handle_change_callback_.Run(
      capture_key_.label, capture_key_.type,
      CreateCaptureHandle(capturer_rfhi, web_contents(), config));
}

void CaptureHandleManager::Observer::UpdateCaptureHandleConfig() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  auto* wc = web_contents();
  if (wc) {
    OnCaptureHandleConfigUpdate(wc->GetPrimaryPage().GetCaptureHandleConfig());
  }
}

CaptureHandleManager::CaptureInfo::CaptureInfo(
    std::unique_ptr<Observer> observer,
    media::mojom::CaptureHandlePtr last_capture_handle,
    DeviceCaptureHandleChangeCallback callback)
    : observer(std::move(observer)),
      last_capture_handle(std::move(last_capture_handle)),
      callback(std::move(callback)) {}

CaptureHandleManager::CaptureInfo::~CaptureInfo() = default;

CaptureHandleManager::CaptureHandleManager() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
}

CaptureHandleManager::~CaptureHandleManager() {
  DCHECK(!BrowserThread::IsThreadInitialized(BrowserThread::IO));
}

void CaptureHandleManager::OnCaptureStarted(
    const std::string& label,
    const blink::MediaStreamDevice& captured_device,
    GlobalRenderFrameHostId capturer,
    DeviceCaptureHandleChangeCallback handle_change_callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  std::optional<GlobalRenderFrameHostId> captured =
      GetCapturedFrameHostId(captured_device.id);
  if (!captured.has_value()) {
    return;
  }

  const CaptureKey capture_key{label, captured_device.type};

  // base::Unretained(this) is safe because the observer is owned by |this|
  // and both live on the UI thread together.
  std::unique_ptr<Observer> observer = Observer::Create(
      capture_key, *captured, capturer,
      base::BindRepeating(&CaptureHandleManager::OnCaptureHandleConfigUpdate,
                          base::Unretained(this)));
  if (!observer) {
    DVLOG(1) << "Observer creation failed.";
    return;
  }

  auto iter = captures_.find(capture_key);
  if (iter == captures_.end()) {
    // Creating a new tracking session.
    const media::mojom::DisplayMediaInformationPtr& info =
        captured_device.display_media_info;
    media::mojom::CaptureHandlePtr capture_handle =
        info ? info->capture_handle.Clone() : nullptr;
    captures_[capture_key] = std::make_unique<CaptureInfo>(
        std::move(observer), std::move(capture_handle),
        std::move(handle_change_callback));
  } else {
    // Updating an existing tracking session in response to a device change.
    iter->second->observer = std::move(observer);
  }

  // The currently executing task comes in response to a chain of tasks juggled
  // between the IO and UI thread. During this time, the CaptureHandleConfig
  // might have changed. Fetch the latest handle.
  captures_[capture_key]->observer->UpdateCaptureHandleConfig();
}

void CaptureHandleManager::OnCaptureStopped(
    const std::string& label,
    const blink::MediaStreamDevice& captured_device) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  captures_.erase({label, captured_device.type});
}

void CaptureHandleManager::OnCaptureDevicesUpdated(
    const std::string& label,
    blink::mojom::StreamDevicesSetPtr new_stream_devices_set,
    GlobalRenderFrameHostId capturer,
    DeviceCaptureHandleChangeCallback handle_change_callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK(new_stream_devices_set);
  DCHECK_EQ(1u, new_stream_devices_set->stream_devices.size());

  // Pause tracking of all old devices.
  for (auto& capture : captures_) {
    if (capture.first.label == label) {
      capture.second->observer = nullptr;
    }
  }

  // Start tracking any new devices; resume tracking of changed devices.
  const blink::mojom::StreamDevices& new_devices =
      *new_stream_devices_set->stream_devices[0];
  if (new_devices.audio_device.has_value()) {
    OnCaptureStarted(label, new_devices.audio_device.value(), capturer,
                     handle_change_callback);
  }
  if (new_devices.video_device.has_value()) {
    OnCaptureStarted(label, new_devices.video_device.value(), capturer,
                     handle_change_callback);
  }

  // Forget any old device which was not in |new_devices|.
  for (auto iter = captures_.begin(); iter != captures_.end();) {
    if (!iter->second->observer) {
      iter = captures_.erase(iter);
    } else {
      ++iter;
    }
  }
}

void CaptureHandleManager::OnCaptureHandleConfigUpdate(
    const std::string& label,
    blink::mojom::MediaStreamType type,
    media::mojom::CaptureHandlePtr capture_handle) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  auto iter = captures_.find({label, type});
  if (iter == captures_.end()) {
    DVLOG(1) << "Unknown session.";
    return;
  }

  const CaptureKey& key = iter->first;
  CaptureInfo& info = *iter->second;

  if (IsEqual(capture_handle, info.last_capture_handle)) {
    // Nothing has changed -> do not report. This avoids exposing navigation
    // between non-exposing sites to a potentially malicious render process.
    return;
  }

  iter->second->last_capture_handle = capture_handle.Clone();

  info.callback.Run(key.label, key.type, std::move(capture_handle));
}

}  // namespace content
