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

#include "third_party/blink/renderer/modules/webusb/usb.h"

#include <utility>

#include "base/feature_list.h"
#include "base/notreached.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "services/device/public/mojom/usb_device.mojom-blink.h"
#include "services/device/public/mojom/usb_enumeration_options.mojom-blink.h"
#include "services/network/public/mojom/permissions_policy/permissions_policy_feature.mojom-blink.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/mojom/service_worker/service_worker.mojom-blink.h"
#include "third_party/blink/public/platform/browser_interface_broker_proxy.h"
#include "third_party/blink/renderer/bindings/core/v8/local_window_proxy.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h"
#include "third_party/blink/renderer/bindings/core/v8/worker_or_worklet_script_controller.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_usb_device_filter.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_usb_device_request_options.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/dom_exception.h"
#include "third_party/blink/renderer/core/execution_context/navigator_base.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/workers/dedicated_worker_global_scope.h"
#include "third_party/blink/renderer/core/workers/worker_global_scope.h"
#include "third_party/blink/renderer/core/workers/worker_or_worklet_global_scope.h"
#include "third_party/blink/renderer/modules/event_target_modules.h"
#include "third_party/blink/renderer/modules/service_worker/service_worker_global_scope.h"
#include "third_party/blink/renderer/modules/webusb/usb_connection_event.h"
#include "third_party/blink/renderer/modules/webusb/usb_device.h"
#include "third_party/blink/renderer/platform/bindings/dom_wrapper_world.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/heap/garbage_collected.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/wtf/functional.h"

using device::mojom::blink::UsbDevice;
using device::mojom::blink::UsbDeviceFilterPtr;
using device::mojom::blink::UsbDeviceInfoPtr;

namespace blink {
namespace {

const char kFeaturePolicyBlocked[] =
    "Access to the feature \"usb\" is disallowed by permissions policy.";
const char kNoDeviceSelected[] = "No device selected.";

void RejectWithTypeError(const String& error_details,
                         ScriptPromiseResolverBase* resolver) {
  ScriptState::Scope scope(resolver->GetScriptState());
  v8::Isolate* isolate = resolver->GetScriptState()->GetIsolate();
  resolver->Reject(V8ThrowException::CreateTypeError(isolate, error_details));
}

UsbDeviceFilterPtr ConvertDeviceFilter(const USBDeviceFilter* filter,
                                       ScriptPromiseResolverBase* resolver) {
  auto mojo_filter = device::mojom::blink::UsbDeviceFilter::New();
  if (filter->hasVendorId()) {
    mojo_filter->vendor_id = filter->vendorId();
  }
  if (filter->hasProductId()) {
    if (!mojo_filter->vendor_id.has_value()) {
      RejectWithTypeError(
          "A filter containing a productId must also contain a vendorId.",
          resolver);
      return nullptr;
    }
    mojo_filter->product_id = filter->productId();
  }
  if (filter->hasClassCode()) {
    mojo_filter->class_code = filter->classCode();
  }
  if (filter->hasSubclassCode()) {
    if (!mojo_filter->class_code.has_value()) {
      RejectWithTypeError(
          "A filter containing a subclassCode must also contain a classCode.",
          resolver);
      return nullptr;
    }
    mojo_filter->subclass_code = filter->subclassCode();
  }
  if (filter->hasProtocolCode()) {
    if (!mojo_filter->subclass_code.has_value()) {
      RejectWithTypeError(
          "A filter containing a protocolCode must also contain a "
          "subclassCode.",
          resolver);
      return nullptr;
    }
    mojo_filter->protocol_code = filter->protocolCode();
  }
  if (filter->hasSerialNumber()) {
    mojo_filter->serial_number = filter->serialNumber();
  }
  return mojo_filter;
}

bool IsContextSupported(ExecutionContext* context) {
  // Since WebUSB on Web Workers is in the process of being implemented, we
  // check here if the runtime flag for the appropriate worker is enabled.
  // TODO(https://crbug.com/837406): Remove this check once the feature has
  // shipped.
  if (!context) {
    return false;
  }

  DCHECK(context->IsWindow() || context->IsDedicatedWorkerGlobalScope() ||
         context->IsServiceWorkerGlobalScope());
  DCHECK(!context->IsDedicatedWorkerGlobalScope() ||
         RuntimeEnabledFeatures::WebUSBOnDedicatedWorkersEnabled());
  DCHECK(!context->IsServiceWorkerGlobalScope() ||
         RuntimeEnabledFeatures::WebUSBOnServiceWorkersEnabled());

  return true;
}

// Carries out basic checks for the web-exposed APIs, to make sure the minimum
// requirements for them to be served are met. Returns true if any conditions
// fail to be met, generating an appropriate exception as well. Otherwise,
// returns false to indicate the call should be allowed.
bool ShouldBlockUsbServiceCall(LocalDOMWindow* window,
                               ExecutionContext* context,
                               ExceptionState* exception_state) {
  if (!IsContextSupported(context)) {
    if (exception_state) {
      exception_state->ThrowDOMException(
          DOMExceptionCode::kNotSupportedError,
          "The implementation did not support the requested type of object or "
          "operation.");
    }
    return true;
  }
  // For window and dedicated workers, reject the request if the top-level frame
  // has an opaque origin. For Service Workers, we use their security origin
  // directly as they do not use delegated permissions.
  const SecurityOrigin* security_origin = nullptr;
  if (context->IsWindow()) {
    security_origin =
        window->GetFrame()->Top()->GetSecurityContext()->GetSecurityOrigin();
  } else if (DedicatedWorkerGlobalScope* dedicated_worker =
                 DynamicTo<DedicatedWorkerGlobalScope>(context)) {
    security_origin = dedicated_worker->top_level_frame_security_origin();
  } else if (context->IsServiceWorkerGlobalScope()) {
    security_origin = context->GetSecurityOrigin();
  } else {
    NOTREACHED();
  }
  if (security_origin->IsOpaque()) {
    if (exception_state) {
      exception_state->ThrowSecurityError(
          "Access to the WebUSB API is denied from contexts where the "
          "top-level document has an opaque origin.");
    }
    return true;
  }

  if (!context->IsFeatureEnabled(network::mojom::PermissionsPolicyFeature::kUsb,
                                 ReportOptions::kReportOnFailure)) {
    if (exception_state) {
      exception_state->ThrowSecurityError(kFeaturePolicyBlocked);
    }
    return true;
  }

  return false;
}

}  // namespace

const char USB::kSupplementName[] = "USB";

USB* USB::usb(NavigatorBase& navigator) {
  USB* usb = Supplement<NavigatorBase>::From<USB>(navigator);
  if (!usb) {
    usb = MakeGarbageCollected<USB>(navigator);
    ProvideTo(navigator, usb);
  }
  return usb;
}

USB::USB(NavigatorBase& navigator)
    : Supplement<NavigatorBase>(navigator),
      ExecutionContextLifecycleObserver(navigator.GetExecutionContext()),
      service_(navigator.GetExecutionContext()),
      client_receiver_(this, navigator.GetExecutionContext()) {}

USB::~USB() {
  // |service_| may still be valid but there should be no more outstanding
  // requests to them because each holds a persistent handle to this object.
  DCHECK(get_devices_requests_.empty());
  DCHECK(get_permission_requests_.empty());
}

ScriptPromise<IDLSequence<USBDevice>> USB::getDevices(
    ScriptState* script_state,
    ExceptionState& exception_state) {
  if (ShouldBlockUsbServiceCall(GetSupplementable()->DomWindow(),
                                GetExecutionContext(), &exception_state)) {
    return ScriptPromise<IDLSequence<USBDevice>>();
  }

  EnsureServiceConnection();
  auto* resolver =
      MakeGarbageCollected<ScriptPromiseResolver<IDLSequence<USBDevice>>>(
          script_state, exception_state.GetContext());
  get_devices_requests_.insert(resolver);
  service_->GetDevices(BindOnce(&USB::OnGetDevices, WrapPersistent(this),
                                WrapPersistent(resolver)));
  return resolver->Promise();
}

ScriptPromise<USBDevice> USB::requestDevice(
    ScriptState* script_state,
    const USBDeviceRequestOptions* options,
    ExceptionState& exception_state) {
  if (!DomWindow()) {
    exception_state.ThrowDOMException(
        DOMExceptionCode::kNotSupportedError,
        "The implementation did not support the requested type of object or "
        "operation.");
    return EmptyPromise();
  }

  if (ShouldBlockUsbServiceCall(GetSupplementable()->DomWindow(),
                                GetExecutionContext(), &exception_state)) {
    return EmptyPromise();
  }

  EnsureServiceConnection();

  if (!LocalFrame::HasTransientUserActivation(DomWindow()->GetFrame())) {
    exception_state.ThrowSecurityError(
        "Must be handling a user gesture to show a permission request.");
    return EmptyPromise();
  }

  auto* resolver = MakeGarbageCollected<ScriptPromiseResolver<USBDevice>>(
      script_state, exception_state.GetContext());
  auto promise = resolver->Promise();
  auto mojo_options = mojom::blink::WebUsbRequestDeviceOptions::New();
  if (options->hasFilters()) {
    mojo_options->filters.reserve(options->filters().size());
    for (const auto& filter : options->filters()) {
      UsbDeviceFilterPtr converted_filter =
          ConvertDeviceFilter(filter, resolver);
      if (!converted_filter)
        return promise;
      mojo_options->filters.push_back(std::move(converted_filter));
    }
  }
  mojo_options->exclusion_filters.reserve(options->exclusionFilters().size());
  for (const auto& filter : options->exclusionFilters()) {
    UsbDeviceFilterPtr converted_filter = ConvertDeviceFilter(filter, resolver);
    if (!converted_filter) {
      return promise;
    }
    mojo_options->exclusion_filters.push_back(std::move(converted_filter));
  }

  DCHECK(options->filters().size() == mojo_options->filters.size());
  DCHECK(options->exclusionFilters().size() ==
         mojo_options->exclusion_filters.size());
  get_permission_requests_.insert(resolver);
  service_->GetPermission(std::move(mojo_options),
                          resolver->WrapCallbackInScriptScope(BindOnce(
                              &USB::OnGetPermission, WrapPersistent(this))));
  return promise;
}

ExecutionContext* USB::GetExecutionContext() const {
  return ExecutionContextLifecycleObserver::GetExecutionContext();
}

const AtomicString& USB::InterfaceName() const {
  return event_target_names::kUSB;
}

void USB::ContextDestroyed() {
  get_devices_requests_.clear();
  get_permission_requests_.clear();
}

void USB::USBDeviceCache::Trace(Visitor* visitor) const {
  visitor->Trace(device_cache_);
}

HeapHashMap<String, WeakMember<USBDevice>>& USB::GetOrCreateWorldDeviceCache(
    DOMWrapperWorld& world) {
  auto it = device_caches_.find(&world);
  if (it != device_caches_.end()) {
    return it->value->DeviceCache();
  }
  auto* cache = MakeGarbageCollected<USBDeviceCache>();
  device_caches_.insert(&world, cache);
  return cache->DeviceCache();
}

USBDevice* USB::GetOrCreateDevice(DOMWrapperWorld& world,
                                  UsbDeviceInfoPtr device_info) {
  auto& device_cache = GetOrCreateWorldDeviceCache(world);
  auto it = device_cache.find(device_info->guid);
  if (it != device_cache.end()) {
    return it->value.Get();
  }

  String guid = device_info->guid;
  mojo::PendingRemote<UsbDevice> pipe;
  service_->GetDevice(guid, pipe.InitWithNewPipeAndPassReceiver());
  USBDevice* device = MakeGarbageCollected<USBDevice>(
      this, std::move(device_info), std::move(pipe), GetExecutionContext());
  device_cache.insert(guid, device);
  return device;
}

USBDevice* USB::GetOrCreateDevice(ScriptState* script_state,
                                  UsbDeviceInfoPtr device_info) {
  if (base::FeatureList::IsEnabled(features::kWebUSBWorldIsolatedCache)) {
    return GetOrCreateDevice(script_state->World(), std::move(device_info));
  }
  return GetOrCreateDevice(std::move(device_info));
}

USBDevice* USB::GetOrCreateDevice(UsbDeviceInfoPtr device_info) {
  auto it = device_cache_.find(device_info->guid);
  if (it != device_cache_.end()) {
    return it->value.Get();
  }

  String guid = device_info->guid;
  mojo::PendingRemote<UsbDevice> pipe;
  service_->GetDevice(guid, pipe.InitWithNewPipeAndPassReceiver());
  USBDevice* device = MakeGarbageCollected<USBDevice>(
      this, std::move(device_info), std::move(pipe), GetExecutionContext());
  device_cache_.insert(guid, device);
  return device;
}

void USB::ForgetDevice(
    const String& device_guid,
    mojom::blink::WebUsbService::ForgetDeviceCallback callback) {
  EnsureServiceConnection();
  service_->ForgetDevice(device_guid, std::move(callback));
}

void USB::OnGetDevices(ScriptPromiseResolver<IDLSequence<USBDevice>>* resolver,
                       Vector<UsbDeviceInfoPtr> device_infos) {
  DCHECK(get_devices_requests_.Contains(resolver));

  ScriptState* script_state = resolver->GetScriptState();
  if (!IsInParallelAlgorithmRunnable(resolver->GetExecutionContext(),
                                     script_state)) {
    return;
  }

  HeapVector<Member<USBDevice>> devices;
  for (auto& device_info : device_infos)
    devices.push_back(GetOrCreateDevice(script_state, std::move(device_info)));
  resolver->Resolve(devices);
  get_devices_requests_.erase(resolver);
}

void USB::OnGetPermission(ScriptPromiseResolver<USBDevice>* resolver,
                          UsbDeviceInfoPtr device_info) {
  DCHECK(get_permission_requests_.Contains(resolver));

  ScriptState* script_state = resolver->GetScriptState();
  if (!IsInParallelAlgorithmRunnable(resolver->GetExecutionContext(),
                                     script_state)) {
    return;
  }

  EnsureServiceConnection();

  if (service_.is_bound() && device_info) {
    resolver->Resolve(GetOrCreateDevice(script_state, std::move(device_info)));
  } else {
    resolver->RejectWithDOMException(DOMExceptionCode::kNotFoundError,
                                     kNoDeviceSelected);
  }
  get_permission_requests_.erase(resolver);
}

void USB::ForEachWorld(base::FunctionRef<void(DOMWrapperWorld*)> action) {
  if (base::FeatureList::IsEnabled(features::kWebUSBWorldIsolatedCache)) {
    ExecutionContext* context = GetExecutionContext();
    if (!context) {
      return;
    }

    if (context->IsWindow()) {
      LocalDOMWindow* window = To<LocalDOMWindow>(context);
      LocalFrame* frame = window->GetFrame();
      if (!frame) {
        return;
      }

      v8::Isolate* isolate = context->GetIsolate();
      v8::HandleScope handle_scope(isolate);
      HeapVector<Member<DOMWrapperWorld>> worlds;
      DOMWrapperWorld::AllWorldsInIsolate(isolate, worlds);

      for (DOMWrapperWorld* world : worlds) {
        LocalWindowProxy* window_proxy =
            frame->WindowProxyMaybeUninitialized(*world);
        if (window_proxy && !window_proxy->ContextIfInitialized().IsEmpty()) {
          action(world);
        }
      }
    } else {
      DCHECK(context->IsWorkerOrWorkletGlobalScope());
      auto* global_scope = To<WorkerOrWorkletGlobalScope>(context);
      ScriptState* script_state =
          global_scope->ScriptController()->GetScriptState();
      if (!script_state) {
        return;
      }
      action(&script_state->World());
    }
  } else {
    action(nullptr);
  }
}

void USB::OnDeviceAdded(UsbDeviceInfoPtr device_info) {
  if (!service_.is_bound())
    return;

  ForEachWorld([this, &device_info](DOMWrapperWorld* world) {
    USBDevice* device;
    if (world) {
      device = GetOrCreateDevice(*world, device_info->Clone());
    } else {
      // `world` is nullptr when `kWebUSBWorldIsolatedCache` is disabled. In
      // that case, we use the shared `device_cache_` (via `GetOrCreateDevice`
      // legacy fallback).
      device = GetOrCreateDevice(std::move(device_info));
    }
    DispatchEvent(
        *USBConnectionEvent::Create(event_type_names::kConnect, device, world));
  });
}

void USB::OnDeviceRemoved(UsbDeviceInfoPtr device_info) {
  String guid = device_info->guid;
  ForEachWorld([this, &device_info, &guid](DOMWrapperWorld* world) {
    USBDevice* device = nullptr;
    // `world` is nullptr when `kWebUSBWorldIsolatedCache` is disabled. In that
    // case, we use the shared `device_cache_`.
    auto& device_cache =
        world ? GetOrCreateWorldDeviceCache(*world) : device_cache_;
    const auto it = device_cache.find(guid);
    if (it != device_cache.end()) {
      device = it->value;
    } else {
      device = MakeGarbageCollected<USBDevice>(
          this, world ? device_info->Clone() : std::move(device_info),
          mojo::NullRemote(), GetExecutionContext());
    }
    DispatchEvent(*USBConnectionEvent::Create(event_type_names::kDisconnect,
                                              device, world));
    device_cache.erase(guid);
  });
}

void USB::OnServiceConnectionError() {
  service_.reset();
  client_receiver_.reset();

  // This loop is resolving promises with a value and so it is possible for
  // script to be executed in the process of determining if the value is a
  // thenable. Move the set to a local variable to prevent such execution from
  // invalidating the iterator used by the loop.
  HeapHashSet<Member<ScriptPromiseResolver<IDLSequence<USBDevice>>>>
      get_devices_requests;
  get_devices_requests.swap(get_devices_requests_);
  for (auto& resolver : get_devices_requests)
    resolver->Resolve(HeapVector<Member<USBDevice>>(0));

  // Similar protection is unnecessary when rejecting a promise.
  for (auto& resolver : get_permission_requests_) {
    ScriptState* resolver_script_state = resolver->GetScriptState();
    if (!IsInParallelAlgorithmRunnable(resolver->GetExecutionContext(),
                                       resolver_script_state)) {
      continue;
    }
    ScriptState::Scope script_state_scope(resolver_script_state);
    resolver->RejectWithDOMException(DOMExceptionCode::kNotFoundError,
                                     kNoDeviceSelected);
  }
}

void USB::AddedEventListener(const AtomicString& event_type,
                             RegisteredEventListener& listener) {
  EventTarget::AddedEventListener(event_type, listener);
  if (event_type != event_type_names::kConnect &&
      event_type != event_type_names::kDisconnect) {
    return;
  }

  auto* context = GetExecutionContext();
  if (ShouldBlockUsbServiceCall(GetSupplementable()->DomWindow(), context,
                                nullptr)) {
    return;
  }

  if (auto* service_worker_global_scope =
          DynamicTo<ServiceWorkerGlobalScope>(context)) {
    if (service_worker_global_scope->did_evaluate_script()) {
      String message = StrCat({"Event handler of '", event_type,
                               "' event must be added on the initial "
                               "evaluation of worker script. More info: "
                               "https://developer.chrome.com/docs/extensions/"
                               "mv3/service_workers/events/"});
      GetExecutionContext()->AddConsoleMessage(
          mojom::blink::ConsoleMessageSource::kJavaScript,
          mojom::blink::ConsoleMessageLevel::kWarning, message);
    }
  }

  EnsureServiceConnection();
}

void USB::EnsureServiceConnection() {
  if (service_.is_bound())
    return;

  DCHECK(IsContextSupported(GetExecutionContext()));
  DCHECK(IsFeatureEnabled(ReportOptions::kDoNotReport));
  // See https://bit.ly/2S0zRAS for task types.
  auto task_runner =
      GetExecutionContext()->GetTaskRunner(TaskType::kMiscPlatformAPI);
  GetExecutionContext()->GetBrowserInterfaceBroker().GetInterface(
      service_.BindNewPipeAndPassReceiver(task_runner));
  service_.set_disconnect_handler(
      BindOnce(&USB::OnServiceConnectionError, WrapWeakPersistent(this)));

  DCHECK(!client_receiver_.is_bound());

  service_->SetClient(
      client_receiver_.BindNewEndpointAndPassRemote(task_runner));
}

bool USB::IsFeatureEnabled(ReportOptions report_options) const {
  return GetExecutionContext()->IsFeatureEnabled(
      network::mojom::PermissionsPolicyFeature::kUsb, report_options);
}

void USB::Trace(Visitor* visitor) const {
  visitor->Trace(service_);
  visitor->Trace(get_devices_requests_);
  visitor->Trace(get_permission_requests_);
  visitor->Trace(client_receiver_);
  visitor->Trace(device_caches_);
  visitor->Trace(device_cache_);
  EventTarget::Trace(visitor);
  Supplement<NavigatorBase>::Trace(visitor);
  ExecutionContextLifecycleObserver::Trace(visitor);
}

}  // namespace blink
