// Copyright 2018 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/usb/web_usb_service_impl.h"

#include <memory>
#include <utility>
#include <vector>

#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "build/android_buildflags.h"
#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/service_worker/service_worker_context_core.h"
#include "content/browser/service_worker/service_worker_usb_delegate_observer.h"
#include "content/browser/service_worker/service_worker_version.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/document_service.h"
#include "content/public/browser/isolated_context_util.h"
#include "content/public/common/content_client.h"
#include "mojo/public/cpp/bindings/associated_remote.h"
#include "services/device/public/mojom/usb_device.mojom.h"
#include "services/device/public/mojom/usb_enumeration_options.mojom.h"
#include "services/device/public/mojom/usb_manager_client.mojom.h"
#include "third_party/blink/public/common/features_generated.h"
#include "third_party/blink/public/mojom/frame/user_activation_notification_type.mojom.h"
#include "third_party/blink/public/mojom/frame/user_activation_update_types.mojom.h"

namespace content {

namespace {

// Deletes the WebUsbService when the connected document is destroyed.
class DocumentHelper : public DocumentService<blink::mojom::WebUsbService> {
 public:
  DocumentHelper(std::unique_ptr<WebUsbServiceImpl> service,
                 RenderFrameHostImpl& render_frame_host,
                 mojo::PendingReceiver<blink::mojom::WebUsbService> receiver)
      : DocumentService(render_frame_host, std::move(receiver)),
        service_(std::move(service)) {
    DCHECK(service_);
  }

  DocumentHelper(const DocumentHelper&) = delete;
  DocumentHelper& operator=(const DocumentHelper&) = delete;

  ~DocumentHelper() override = default;

  // blink::mojom::WebUsbService:
  void GetDevices(GetDevicesCallback callback) override {
    service_->GetDevices(std::move(callback));
  }
  void GetDevice(const std::string& guid,
                 mojo::PendingReceiver<device::mojom::UsbDevice>
                     device_receiver) override {
    service_->GetDevice(guid, std::move(device_receiver));
  }
  void GetPermission(
      blink::mojom::WebUsbRequestDeviceOptionsPtr options,
      blink::mojom::WebUsbService::GetPermissionCallback callback) override {
    service_->GetPermission(std::move(options), std::move(callback));
  }
  void ForgetDevice(
      const std::string& guid,
      blink::mojom::WebUsbService::ForgetDeviceCallback callback) override {
    service_->ForgetDevice(guid, std::move(callback));
  }
  void SetClient(
      mojo::PendingAssociatedRemote<device::mojom::UsbDeviceManagerClient>
          client) override {
    service_->SetClient(std::move(client));
  }

 private:
  const std::unique_ptr<WebUsbServiceImpl> service_;
};

}  // namespace

// A UsbDeviceClient represents a UsbDevice pipe that has been passed to the
// renderer process. The UsbDeviceClient pipe allows the browser process to
// continue to monitor how the device is used and cause the connection to be
// closed at will.
class WebUsbServiceImpl::UsbDeviceClient
    : public device::mojom::UsbDeviceClient {
 public:
  UsbDeviceClient(
      WebUsbServiceImpl* service,
      const std::string& device_guid,
      mojo::PendingReceiver<device::mojom::UsbDeviceClient> receiver)
      : service_(service),
        device_guid_(device_guid),
        receiver_(this, std::move(receiver)) {
    receiver_.set_disconnect_handler(
        base::BindOnce(&WebUsbServiceImpl::RemoveDeviceClient,
                       base::Unretained(service_), base::Unretained(this)));
  }

  UsbDeviceClient(const UsbDeviceClient&) = delete;
  UsbDeviceClient& operator=(const UsbDeviceClient&) = delete;

  ~UsbDeviceClient() override {
    if (opened_) {
      // If the connection was opened destroying |receiver_| will cause it to
      // be closed but that event won't be dispatched here because the receiver
      // has been destroyed.
      OnDeviceClosed();
    }
  }

  const std::string& device_guid() const { return device_guid_; }

  // device::mojom::UsbDeviceClient implementation:
  void OnDeviceOpened() override {
    DCHECK(!opened_);
    opened_ = true;
    service_->IncrementConnectionCount();
  }

  void OnDeviceClosed() override {
    DCHECK(opened_);
    opened_ = false;
    service_->DecrementConnectionCount();
  }

 private:
  const raw_ptr<WebUsbServiceImpl> service_;
  const std::string device_guid_;
  bool opened_ = false;
  mojo::Receiver<device::mojom::UsbDeviceClient> receiver_;
};

WebUsbServiceImpl::WebUsbServiceImpl(
    RenderFrameHostImpl* render_frame_host,
    base::WeakPtr<ServiceWorkerVersion> service_worker_version,
    const url::Origin& origin)
    : render_frame_host_(render_frame_host),
      service_worker_version_(std::move(service_worker_version)),
      origin_(origin) {
  auto* delegate = GetContentClient()->browser()->GetUsbDelegate();
  if (delegate && render_frame_host_) {
    delegate->AddObserver(GetBrowserContext(), this);
  } else if (service_worker_version_) {
#if !BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_DESKTOP_ANDROID)
    // For service worker case, it relies on ServiceWorkerUsbDelegateObserver to
    // be the broker between UsbDelegate and UsbService. This is limited to
    // platforms that support extensions.
    auto context = service_worker_version_->context();
    if (context) {
      context->usb_delegate_observer()->RegisterUsbService(
          service_worker_version_->registration_id(),
          weak_factory_.GetWeakPtr());
    }
#else
    NOTREACHED();
#endif  // !BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_DESKTOP_ANDROID)
  }
}

WebUsbServiceImpl::~WebUsbServiceImpl() {
  auto* delegate = GetContentClient()->browser()->GetUsbDelegate();
  if (delegate && render_frame_host_) {
    delegate->RemoveObserver(GetBrowserContext(), this);
  }
}

// static
void WebUsbServiceImpl::Create(
    RenderFrameHostImpl& render_frame_host,
    mojo::PendingReceiver<blink::mojom::WebUsbService> pending_receiver) {
  if (!render_frame_host.IsFeatureEnabled(
          network::mojom::PermissionsPolicyFeature::kUsb)) {
    mojo::ReportBadMessage("Permissions policy blocks access to USB.");
    return;
  }

  // Avoid creating the WebUsbService if there is no USB delegate to provide the
  // implementation.
  UsbDelegate* delegate = GetContentClient()->browser()->GetUsbDelegate();
  if (!delegate) {
    return;
  }

  if (render_frame_host.IsNestedWithinFencedFrame()) {
    // The renderer is supposed to disallow the use of USB services when inside
    // a fenced frame. Anything getting past the renderer checks must be marked
    // as a bad request.
    mojo::ReportBadMessage("WebUSB is not allowed in a fenced frame tree.");
    return;
  }

  if (!delegate->PageMayUseUsb(render_frame_host.GetPage())) {
    return;
  }

  // DocumentHelper observes the lifetime of the document connected to
  // `render_frame_host` and destroys the WebUsbService when the Mojo connection
  // is disconnected, RenderFrameHost is deleted, or the RenderFrameHost commits
  // a cross-document navigation. It forwards its Mojo interface to
  // WebUsbServiceImpl.
  new DocumentHelper(
      std::make_unique<WebUsbServiceImpl>(
          &render_frame_host,
          /*service_worker_version=*/nullptr,
          render_frame_host.GetMainFrame()->GetLastCommittedOrigin()),
      render_frame_host, std::move(pending_receiver));
}

// static
void WebUsbServiceImpl::Create(
    base::WeakPtr<ServiceWorkerVersion> service_worker_version,
    const url::Origin& origin,
    mojo::PendingReceiver<blink::mojom::WebUsbService> pending_receiver) {
  DCHECK(service_worker_version);

  // Avoid creating the WebUsbService if there is no USB delegate to provide
  // the implementation or if `origin` is not eligible to access WebUSB from a
  // service worker.
  auto* delegate = GetContentClient()->browser()->GetUsbDelegate();
  if (!delegate || !delegate->IsServiceWorkerAllowedForOrigin(origin)) {
    return;
  }

  // This makes the WebUsbService a self-owned receiver so it will self-destruct
  // when a mojo interface error occurs.
  mojo::MakeSelfOwnedReceiver(std::make_unique<WebUsbServiceImpl>(
                                  /*render_frame_host=*/nullptr,
                                  std::move(service_worker_version), origin),
                              std::move(pending_receiver));
}

BrowserContext* WebUsbServiceImpl::GetBrowserContext() const {
  if (render_frame_host_) {
    return render_frame_host_->GetBrowserContext();
  }
  if (service_worker_version_ && service_worker_version_->context()) {
    return service_worker_version_->context()->wrapper()->browser_context();
  }
  return nullptr;
}

std::vector<uint8_t> WebUsbServiceImpl::GetProtectedInterfaceClasses() const {
  // Specified in https://wicg.github.io/webusb#protected-interface-classes
  std::vector<uint8_t> classes = {
      device::mojom::kUsbAudioClass,       device::mojom::kUsbHidClass,
      device::mojom::kUsbMassStorageClass, device::mojom::kUsbSmartCardClass,
      device::mojom::kUsbVideoClass,       device::mojom::kUsbAudioVideoClass,
      device::mojom::kUsbWirelessClass,
  };

  auto* delegate = GetContentClient()->browser()->GetUsbDelegate();
  if (delegate) {
    delegate->AdjustProtectedInterfaceClasses(GetBrowserContext(), origin_,
                                              render_frame_host_, classes);
  }

  // If the 'kUnrestrictedUsb' feature is enabled and the isolated context has
  // 'kUsbUnrestricted' permission, grant access to all USB interface classes.
  bool is_usb_unrestricted = false;
  if (base::FeatureList::IsEnabled(blink::features::kUnrestrictedUsb)) {
    is_usb_unrestricted =
        render_frame_host_ &&
        render_frame_host_->IsFeatureEnabled(
            network::mojom::PermissionsPolicyFeature::kUsbUnrestricted) &&
        HasIsolatedContextCapability(render_frame_host_);
  }
  if (is_usb_unrestricted) {
    classes.clear();
  }

  return classes;
}

void WebUsbServiceImpl::GetDevices(GetDevicesCallback callback) {
  auto* delegate = GetContentClient()->browser()->GetUsbDelegate();
  if (!delegate) {
    std::move(callback).Run(std::vector<device::mojom::UsbDeviceInfoPtr>());
    return;
  }

  delegate->GetDevices(
      GetBrowserContext(),
      base::BindOnce(&WebUsbServiceImpl::OnGetDevices,
                     weak_factory_.GetWeakPtr(), std::move(callback)));
}

void WebUsbServiceImpl::OnGetDevices(
    GetDevicesCallback callback,
    std::vector<device::mojom::UsbDeviceInfoPtr> device_info_list) {
  auto* delegate = GetContentClient()->browser()->GetUsbDelegate();
  DCHECK(delegate);

  std::vector<device::mojom::UsbDeviceInfoPtr> device_infos;
  for (auto& device_info : device_info_list) {
    if (delegate->HasDevicePermission(GetBrowserContext(), render_frame_host_,
                                      origin_, *device_info)) {
      device_infos.push_back(device_info.Clone());
    }
  }
  std::move(callback).Run(std::move(device_infos));
}

void WebUsbServiceImpl::GetDevice(
    const std::string& guid,
    mojo::PendingReceiver<device::mojom::UsbDevice> device_receiver) {
  auto* delegate = GetContentClient()->browser()->GetUsbDelegate();
  if (!delegate) {
    return;
  }

  auto* browser_context = GetBrowserContext();
  auto* device_info = delegate->GetDeviceInfo(browser_context, guid);
  if (!device_info ||
      !delegate->HasDevicePermission(browser_context, render_frame_host_,
                                     origin_, *device_info)) {
    return;
  }

  // Connect Blink to the native device and keep a receiver to this for the
  // UsbDeviceClient interface so we can receive DeviceOpened/Closed events.
  // This receiver will also be closed to notify the device service to close
  // the connection if permission is revoked.
  mojo::PendingRemote<device::mojom::UsbDeviceClient> device_client;
  device_clients_.push_back(std::make_unique<UsbDeviceClient>(
      this, guid, device_client.InitWithNewPipeAndPassReceiver()));

  delegate->GetDevice(GetBrowserContext(), guid, GetProtectedInterfaceClasses(),
                      std::move(device_receiver), std::move(device_client));
}

void WebUsbServiceImpl::GetPermission(
    blink::mojom::WebUsbRequestDeviceOptionsPtr options,
    GetPermissionCallback callback) {
  auto* delegate = GetContentClient()->browser()->GetUsbDelegate();
  if (!delegate ||
      !delegate->CanRequestDevicePermission(GetBrowserContext(), origin_)) {
    std::move(callback).Run(nullptr);
    return;
  }

  if (!render_frame_host_) {
    mojo::ReportBadMessage(
        "GetPermission is not allowed from a service worker.");
    std::move(callback).Run(nullptr);
    return;
  }

  if (!FrameTreeNode::From(render_frame_host_)
           ->UpdateUserActivationState(
               blink::mojom::UserActivationUpdateType::
                   kConsumeTransientActivation,
               blink::mojom::UserActivationNotificationType::kNone)) {
    std::move(callback).Run(nullptr);
    return;
  }

  // The delegate's chooser implementation may spin a nested message loop (e.g.
  // to drop fullscreen), during which the frame may be detached and the
  // service destroyed. Check that the service is still alive before accessing
  // member variables.
  base::WeakPtr<WebUsbServiceImpl> weak_this = weak_factory_.GetWeakPtr();
  auto chooser = delegate->RunChooser(*render_frame_host_, std::move(options),
                                      std::move(callback));
  if (!weak_this) {
    return;
  }
  usb_chooser_ = std::move(chooser);
}

void WebUsbServiceImpl::ForgetDevice(const std::string& guid,
                                     ForgetDeviceCallback callback) {
  auto* delegate = GetContentClient()->browser()->GetUsbDelegate();
  if (delegate) {
    auto* browser_context = GetBrowserContext();
    auto* device_info = delegate->GetDeviceInfo(browser_context, guid);
    if (device_info &&
        delegate->HasDevicePermission(browser_context, render_frame_host_,
                                      origin_, *device_info)) {
      delegate->RevokeDevicePermissionWebInitiated(browser_context, origin_,
                                                   *device_info);
    }
  }
  std::move(callback).Run();
}

void WebUsbServiceImpl::SetClient(
    mojo::PendingAssociatedRemote<device::mojom::UsbDeviceManagerClient>
        client) {
  DCHECK(client);
  clients_.Add(std::move(client));
#if !BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_DESKTOP_ANDROID)
  if (service_worker_version_ && service_worker_version_->context()) {
    // WebUsbService is expected to have only one DeviceManagerClient when it is
    // for a service worker. One renderer side of a service worker has its own
    // associated WebUsbService.
    CHECK_EQ(1u, clients_.size());
    // When a service worker is woken up by a device connection event, the
    // client might not have yet registered with the WebUsbService or the
    // WebUsbService hasn't been created yet when service worker is in running
    // state. This is because service worker is set to running state after
    // script evaluation but inter-processes request triggered from the script
    // evaluation that creates WebUsbService or registers a client might not be
    // done in the browser process. To handle this situation, pending callbacks
    // are stored and to be processed when registering the client.
    service_worker_version_->context()
        ->usb_delegate_observer()
        ->ProcessPendingCallbacks(service_worker_version_.get());
  }
#endif  // !BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_DESKTOP_ANDROID)
}

void WebUsbServiceImpl::OnPermissionRevoked(const url::Origin& origin) {
  if (origin_ != origin) {
    return;
  }

  // Close the connection between Blink and the device if the device lost
  // permission.
  auto* delegate = GetContentClient()->browser()->GetUsbDelegate();
  auto* browser_context = GetBrowserContext();
  std::erase_if(device_clients_, [=, this](const auto& client) {
    auto* device_info =
        delegate->GetDeviceInfo(browser_context, client->device_guid());
    if (!device_info)
      return true;

    return !delegate->HasDevicePermission(browser_context, render_frame_host_,
                                          origin_, *device_info);
  });
}

void WebUsbServiceImpl::OnDeviceAdded(
    const device::mojom::UsbDeviceInfo& device_info) {
  if (!GetContentClient()->browser()->GetUsbDelegate()->HasDevicePermission(
          GetBrowserContext(), render_frame_host_, origin_, device_info)) {
    return;
  }
  for (auto& client : clients_)
    client->OnDeviceAdded(device_info.Clone());
}

void WebUsbServiceImpl::OnDeviceRemoved(
    const device::mojom::UsbDeviceInfo& device_info) {
  std::erase_if(device_clients_, [&device_info](const auto& client) {
    return device_info.guid == client->device_guid();
  });

  if (!GetContentClient()->browser()->GetUsbDelegate()->HasDevicePermission(
          GetBrowserContext(), render_frame_host_, origin_, device_info)) {
    return;
  }
  for (auto& client : clients_)
    client->OnDeviceRemoved(device_info.Clone());
}

void WebUsbServiceImpl::OnDeviceManagerConnectionError() {
  // Close the connection with blink.
  clients_.Clear();
}

// device::mojom::UsbDeviceClient implementation:
void WebUsbServiceImpl::IncrementConnectionCount() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  auto* delegate = GetContentClient()->browser()->GetUsbDelegate();
  if (delegate) {
    delegate->IncrementConnectionCount(GetBrowserContext(), origin_);
  }

  if (connection_count_++ == 0) {
    if (render_frame_host_) {
      auto* web_contents = static_cast<WebContentsImpl*>(
          WebContents::FromRenderFrameHost(render_frame_host_));
      web_contents->IncrementUsbActiveFrameCount();
    } else if (service_worker_version_) {
      CHECK(!service_worker_activity_request_uuid_);
      service_worker_activity_request_uuid_ = base::Uuid::GenerateRandomV4();
      service_worker_version_->StartExternalRequest(
          *service_worker_activity_request_uuid_,
          ServiceWorkerExternalRequestTimeoutType::kDoesNotTimeout);
    }
  }
}

void WebUsbServiceImpl::DecrementConnectionCount() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  auto* delegate = GetContentClient()->browser()->GetUsbDelegate();
  if (delegate) {
    delegate->DecrementConnectionCount(GetBrowserContext(), origin_);
  }

  DCHECK_GT(connection_count_, 0);
  if (--connection_count_ == 0) {
    if (render_frame_host_) {
      auto* web_contents = static_cast<WebContentsImpl*>(
          WebContents::FromRenderFrameHost(render_frame_host_));
      web_contents->DecrementUsbActiveFrameCount();
    } else if (service_worker_version_) {
      CHECK(service_worker_activity_request_uuid_);
      service_worker_version_->FinishExternalRequest(
          *service_worker_activity_request_uuid_);
      service_worker_activity_request_uuid_.reset();
    }
  }
}

void WebUsbServiceImpl::RemoveDeviceClient(const UsbDeviceClient* client) {
  std::erase_if(device_clients_, [client](const auto& this_client) {
    return client == this_client.get();
  });
}

}  // namespace content
