blob: 471f0e64fe17b48bd893b5c07d8a13517cbf85f2 [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// 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 "device/usb/public/mojom/device.mojom-blink.h"
#include "services/service_manager/public/cpp/interface_provider.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/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/dom_exception.h"
#include "third_party/blink/renderer/core/frame/frame.h"
#include "third_party/blink/renderer/modules/event_target_modules.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/modules/webusb/usb_device_filter.h"
#include "third_party/blink/renderer/modules/webusb/usb_device_request_options.h"
#include "third_party/blink/renderer/platform/feature_policy/feature_policy.h"
#include "third_party/blink/renderer/platform/mojo/mojo_helper.h"
#include "third_party/blink/renderer/platform/wtf/functional.h"
using device::mojom::blink::UsbDeviceFilterPtr;
using device::mojom::blink::UsbDeviceInfoPtr;
using device::mojom::blink::UsbDevicePtr;
namespace blink {
namespace {
const char kFeaturePolicyBlocked[] =
"Access to the feature \"usb\" is disallowed by feature policy.";
const char kNoDeviceSelected[] = "No device selected.";
UsbDeviceFilterPtr ConvertDeviceFilter(const USBDeviceFilter& filter) {
auto mojo_filter = device::mojom::blink::UsbDeviceFilter::New();
mojo_filter->has_vendor_id = filter.hasVendorId();
if (mojo_filter->has_vendor_id)
mojo_filter->vendor_id = filter.vendorId();
mojo_filter->has_product_id = filter.hasProductId();
if (mojo_filter->has_product_id)
mojo_filter->product_id = filter.productId();
mojo_filter->has_class_code = filter.hasClassCode();
if (mojo_filter->has_class_code)
mojo_filter->class_code = filter.classCode();
mojo_filter->has_subclass_code = filter.hasSubclassCode();
if (mojo_filter->has_subclass_code)
mojo_filter->subclass_code = filter.subclassCode();
mojo_filter->has_protocol_code = filter.hasProtocolCode();
if (mojo_filter->has_protocol_code)
mojo_filter->protocol_code = filter.protocolCode();
if (filter.hasSerialNumber())
mojo_filter->serial_number = filter.serialNumber();
return mojo_filter;
}
} // namespace
USB::USB(ExecutionContext& context)
: ContextLifecycleObserver(&context), client_binding_(this) {}
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_.IsEmpty());
DCHECK(get_permission_requests_.IsEmpty());
}
void USB::Dispose() {
// The pipe to this object must be closed when is marked unreachable to
// prevent messages from being dispatched before lazy sweeping.
client_binding_.Close();
}
ScriptPromise USB::getDevices(ScriptState* script_state) {
if (!IsContextSupported()) {
return ScriptPromise::RejectWithDOMException(
script_state,
DOMException::Create(DOMExceptionCode::kNotSupportedError));
}
if (!IsFeatureEnabled()) {
return ScriptPromise::RejectWithDOMException(
script_state, DOMException::Create(DOMExceptionCode::kSecurityError,
kFeaturePolicyBlocked));
}
EnsureServiceConnection();
ScriptPromiseResolver* resolver = ScriptPromiseResolver::Create(script_state);
get_devices_requests_.insert(resolver);
service_->GetDevices(WTF::Bind(&USB::OnGetDevices, WrapPersistent(this),
WrapPersistent(resolver)));
return resolver->Promise();
}
ScriptPromise USB::requestDevice(ScriptState* script_state,
const USBDeviceRequestOptions& options) {
LocalFrame* frame = GetFrame();
if (!frame) {
return ScriptPromise::RejectWithDOMException(
script_state,
DOMException::Create(DOMExceptionCode::kNotSupportedError));
}
if (!frame->IsFeatureEnabled(mojom::FeaturePolicyFeature::kUsb)) {
return ScriptPromise::RejectWithDOMException(
script_state, DOMException::Create(DOMExceptionCode::kSecurityError,
kFeaturePolicyBlocked));
}
EnsureServiceConnection();
if (!Frame::HasTransientUserActivation(frame)) {
return ScriptPromise::RejectWithDOMException(
script_state,
DOMException::Create(
DOMExceptionCode::kSecurityError,
"Must be handling a user gesture to show a permission request."));
}
Vector<UsbDeviceFilterPtr> filters;
if (options.hasFilters()) {
filters.ReserveCapacity(options.filters().size());
for (const auto& filter : options.filters())
filters.push_back(ConvertDeviceFilter(filter));
}
ScriptPromiseResolver* resolver = ScriptPromiseResolver::Create(script_state);
get_permission_requests_.insert(resolver);
service_->GetPermission(std::move(filters),
WTF::Bind(&USB::OnGetPermission, WrapPersistent(this),
WrapPersistent(resolver)));
return resolver->Promise();
}
ExecutionContext* USB::GetExecutionContext() const {
return ContextLifecycleObserver::GetExecutionContext();
}
const AtomicString& USB::InterfaceName() const {
return EventTargetNames::USB;
}
void USB::ContextDestroyed(ExecutionContext*) {
service_.reset();
get_devices_requests_.clear();
get_permission_requests_.clear();
}
USBDevice* USB::GetOrCreateDevice(UsbDeviceInfoPtr device_info) {
USBDevice* device = device_cache_.at(device_info->guid);
if (!device) {
String guid = device_info->guid;
UsbDevicePtr pipe;
service_->GetDevice(guid, mojo::MakeRequest(&pipe));
device = USBDevice::Create(std::move(device_info), std::move(pipe),
GetExecutionContext());
device_cache_.insert(guid, device);
}
return device;
}
void USB::OnGetDevices(ScriptPromiseResolver* resolver,
Vector<UsbDeviceInfoPtr> device_infos) {
DCHECK(get_devices_requests_.Contains(resolver));
HeapVector<Member<USBDevice>> devices;
for (auto& device_info : device_infos)
devices.push_back(GetOrCreateDevice(std::move(device_info)));
resolver->Resolve(devices);
get_devices_requests_.erase(resolver);
}
void USB::OnGetPermission(ScriptPromiseResolver* resolver,
UsbDeviceInfoPtr device_info) {
DCHECK(get_permission_requests_.Contains(resolver));
EnsureServiceConnection();
if (service_ && device_info) {
resolver->Resolve(GetOrCreateDevice(std::move(device_info)));
} else {
resolver->Reject(DOMException::Create(DOMExceptionCode::kNotFoundError,
kNoDeviceSelected));
}
get_permission_requests_.erase(resolver);
}
void USB::OnDeviceAdded(UsbDeviceInfoPtr device_info) {
if (!service_)
return;
DispatchEvent(*USBConnectionEvent::Create(
EventTypeNames::connect, GetOrCreateDevice(std::move(device_info))));
}
void USB::OnDeviceRemoved(UsbDeviceInfoPtr device_info) {
String guid = device_info->guid;
USBDevice* device = device_cache_.at(guid);
if (!device) {
device = USBDevice::Create(std::move(device_info), nullptr,
GetExecutionContext());
}
DispatchEvent(
*USBConnectionEvent::Create(EventTypeNames::disconnect, device));
device_cache_.erase(guid);
}
void USB::OnServiceConnectionError() {
service_.reset();
client_binding_.Close();
for (ScriptPromiseResolver* resolver : get_devices_requests_)
resolver->Resolve(HeapVector<Member<USBDevice>>(0));
get_devices_requests_.clear();
for (ScriptPromiseResolver* resolver : get_permission_requests_) {
resolver->Reject(DOMException::Create(DOMExceptionCode::kNotFoundError,
kNoDeviceSelected));
}
get_permission_requests_.clear();
}
void USB::AddedEventListener(const AtomicString& event_type,
RegisteredEventListener& listener) {
EventTargetWithInlineData::AddedEventListener(event_type, listener);
if (event_type != EventTypeNames::connect &&
event_type != EventTypeNames::disconnect) {
return;
}
if (!IsContextSupported() || !IsFeatureEnabled())
return;
EnsureServiceConnection();
}
void USB::EnsureServiceConnection() {
if (service_)
return;
DCHECK(IsContextSupported());
DCHECK(IsFeatureEnabled());
GetExecutionContext()->GetInterfaceProvider()->GetInterface(
mojo::MakeRequest(&service_));
service_.set_connection_error_handler(
WTF::Bind(&USB::OnServiceConnectionError, WrapWeakPersistent(this)));
DCHECK(!client_binding_.is_bound());
device::mojom::blink::UsbDeviceManagerClientPtr client;
client_binding_.Bind(mojo::MakeRequest(&client));
service_->SetClient(std::move(client));
}
bool USB::IsContextSupported() const {
// 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.
ExecutionContext* context = GetExecutionContext();
if (!context)
return false;
DCHECK(context->IsDocument() || context->IsDedicatedWorkerGlobalScope());
DCHECK(!context->IsDedicatedWorkerGlobalScope() ||
RuntimeEnabledFeatures::WebUSBOnDedicatedWorkersEnabled());
return true;
}
bool USB::IsFeatureEnabled() const {
ExecutionContext* context = GetExecutionContext();
FeaturePolicy* policy = context->GetSecurityContext().GetFeaturePolicy();
return policy->IsFeatureEnabled(mojom::FeaturePolicyFeature::kUsb);
}
void USB::Trace(blink::Visitor* visitor) {
visitor->Trace(get_devices_requests_);
visitor->Trace(get_permission_requests_);
visitor->Trace(device_cache_);
EventTargetWithInlineData::Trace(visitor);
ContextLifecycleObserver::Trace(visitor);
}
} // namespace blink