blob: c19db37947cada778efc5cf5f3545f06e5ad04ef [file] [log] [blame]
// Copyright 2019 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/nfc/ndef_reader.h"
#include <memory>
#include "services/device/public/mojom/nfc.mojom-blink.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/v8_throw_dom_exception.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_ndef_make_read_only_options.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_ndef_scan_options.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_ndef_write_options.h"
#include "third_party/blink/renderer/core/dom/abort_signal.h"
#include "third_party/blink/renderer/core/dom/dom_exception.h"
#include "third_party/blink/renderer/core/dom/scoped_abort_state.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/inspector/console_message.h"
#include "third_party/blink/renderer/modules/event_target_modules.h"
#include "third_party/blink/renderer/modules/nfc/ndef_message.h"
#include "third_party/blink/renderer/modules/nfc/ndef_reading_event.h"
#include "third_party/blink/renderer/modules/nfc/nfc_proxy.h"
#include "third_party/blink/renderer/modules/nfc/nfc_type_converters.h"
#include "third_party/blink/renderer/modules/permissions/permission_utils.h"
#include "third_party/blink/renderer/platform/instrumentation/use_counter.h"
#include "third_party/blink/renderer/platform/scheduler/public/frame_scheduler.h"
#include "third_party/blink/renderer/platform/scheduler/public/scheduling_policy.h"
namespace blink {
using mojom::blink::PermissionName;
using mojom::blink::PermissionService;
namespace {
v8::Local<v8::Value> NDEFErrorTypeToDOMException(
v8::Isolate* isolate,
device::mojom::blink::NDEFErrorType error_type,
const String& error_message) {
switch (error_type) {
case device::mojom::blink::NDEFErrorType::NOT_ALLOWED:
return V8ThrowDOMException::CreateOrDie(
isolate, DOMExceptionCode::kNotAllowedError, error_message);
case device::mojom::blink::NDEFErrorType::NOT_SUPPORTED:
return V8ThrowDOMException::CreateOrDie(
isolate, DOMExceptionCode::kNotSupportedError, error_message);
case device::mojom::blink::NDEFErrorType::NOT_READABLE:
return V8ThrowDOMException::CreateOrDie(
isolate, DOMExceptionCode::kNotReadableError, error_message);
case device::mojom::blink::NDEFErrorType::INVALID_MESSAGE:
return V8ThrowDOMException::CreateOrDie(
isolate, DOMExceptionCode::kSyntaxError, error_message);
case device::mojom::blink::NDEFErrorType::OPERATION_CANCELLED:
return V8ThrowDOMException::CreateOrDie(
isolate, DOMExceptionCode::kAbortError, error_message);
case device::mojom::blink::NDEFErrorType::IO_ERROR:
return V8ThrowDOMException::CreateOrDie(
isolate, DOMExceptionCode::kNetworkError, error_message);
}
NOTREACHED();
// Don't need to handle the case after a NOTREACHED().
return v8::Local<v8::Value>();
}
v8::Local<v8::Value> NDEFErrorPtrToDOMException(
v8::Isolate* isolate,
device::mojom::blink::NDEFErrorPtr error) {
return NDEFErrorTypeToDOMException(isolate, error->error_type,
error->error_message);
}
constexpr char kNotSupportedOrPermissionDenied[] =
"Web NFC is unavailable or permission denied.";
constexpr char kChildFrameErrorMessage[] =
"Web NFC can only be accessed in a top-level browsing context.";
} // namespace
class NDEFReader::ReadAbortAlgorithm final : public AbortSignal::Algorithm {
public:
ReadAbortAlgorithm(NDEFReader* ndef_reader, AbortSignal* signal)
: ndef_reader_(ndef_reader), abort_signal_(signal) {}
~ReadAbortAlgorithm() override = default;
void Run() override { ndef_reader_->ReadAbort(abort_signal_); }
void Trace(Visitor* visitor) const override {
visitor->Trace(ndef_reader_);
visitor->Trace(abort_signal_);
Algorithm::Trace(visitor);
}
private:
Member<NDEFReader> ndef_reader_;
Member<AbortSignal> abort_signal_;
};
class NDEFReader::WriteAbortAlgorithm final : public AbortSignal::Algorithm {
public:
explicit WriteAbortAlgorithm(NDEFReader* ndef_reader)
: ndef_reader_(ndef_reader) {}
~WriteAbortAlgorithm() override = default;
void Run() override { ndef_reader_->WriteAbort(); }
void Trace(Visitor* visitor) const override {
visitor->Trace(ndef_reader_);
Algorithm::Trace(visitor);
}
private:
Member<NDEFReader> ndef_reader_;
};
class NDEFReader::MakeReadOnlyAbortAlgorithm final
: public AbortSignal::Algorithm {
public:
explicit MakeReadOnlyAbortAlgorithm(NDEFReader* ndef_reader)
: ndef_reader_(ndef_reader) {}
~MakeReadOnlyAbortAlgorithm() override = default;
void Run() override { ndef_reader_->MakeReadOnlyAbort(); }
void Trace(Visitor* visitor) const override {
visitor->Trace(ndef_reader_);
Algorithm::Trace(visitor);
}
private:
Member<NDEFReader> ndef_reader_;
};
// static
NDEFReader* NDEFReader::Create(ExecutionContext* context) {
context->GetScheduler()->RegisterStickyFeature(
SchedulingPolicy::Feature::kWebNfc,
{SchedulingPolicy::DisableBackForwardCache()});
return MakeGarbageCollected<NDEFReader>(context);
}
NDEFReader::NDEFReader(ExecutionContext* context)
: ActiveScriptWrappable<NDEFReader>({}),
ExecutionContextLifecycleObserver(context),
nfc_proxy_(NFCProxy::From(*DomWindow())),
permission_service_(context) {}
NDEFReader::~NDEFReader() = default;
const AtomicString& NDEFReader::InterfaceName() const {
return event_target_names::kNDEFReader;
}
ExecutionContext* NDEFReader::GetExecutionContext() const {
return ExecutionContextLifecycleObserver::GetExecutionContext();
}
bool NDEFReader::HasPendingActivity() const {
return GetExecutionContext() && nfc_proxy_->IsReading(this) &&
HasEventListeners();
}
// https://w3c.github.io/web-nfc/#the-scan-method
ScriptPromise NDEFReader::scan(ScriptState* script_state,
const NDEFScanOptions* options,
ExceptionState& exception_state) {
// https://w3c.github.io/web-nfc/#security-policies
// WebNFC API must be only accessible from top level browsing context.
if (!DomWindow() || !DomWindow()->GetFrame()->IsMainFrame()) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
kChildFrameErrorMessage);
return ScriptPromise();
}
if (scan_signal_ && scan_abort_handle_) {
scan_signal_->RemoveAlgorithm(scan_abort_handle_);
scan_abort_handle_.Clear();
}
scan_signal_ = options->getSignalOr(nullptr);
if (scan_signal_) {
if (scan_signal_->aborted()) {
return ScriptPromise::Reject(script_state,
scan_signal_->reason(script_state));
}
scan_abort_handle_ = scan_signal_->AddAlgorithm(
MakeGarbageCollected<ReadAbortAlgorithm>(this, scan_signal_));
}
// Reject promise when there's already an ongoing scan.
if (scan_resolver_ || nfc_proxy_->IsReading(this)) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
"A scan() operation is ongoing.");
return ScriptPromise();
}
scan_resolver_ = MakeGarbageCollected<ScriptPromiseResolver>(
script_state, exception_state.GetContext());
GetPermissionService()->RequestPermission(
CreatePermissionDescriptor(PermissionName::NFC),
LocalFrame::HasTransientUserActivation(DomWindow()->GetFrame()),
WTF::BindOnce(&NDEFReader::ReadOnRequestPermission, WrapPersistent(this),
WrapPersistent(options)));
return scan_resolver_->Promise();
}
void NDEFReader::ReadOnRequestPermission(
const NDEFScanOptions* options,
mojom::blink::PermissionStatus status) {
if (!scan_resolver_)
return;
ScriptState* script_state = scan_resolver_->GetScriptState();
if (!IsInParallelAlgorithmRunnable(scan_resolver_->GetExecutionContext(),
script_state)) {
scan_resolver_.Clear();
return;
}
ScriptState::Scope script_state_scope(script_state);
if (status != mojom::blink::PermissionStatus::GRANTED) {
scan_resolver_->Reject(V8ThrowDOMException::CreateOrDie(
script_state->GetIsolate(), DOMExceptionCode::kNotAllowedError,
"NFC permission request denied."));
scan_resolver_.Clear();
return;
}
DCHECK(!scan_signal_ || !scan_signal_->aborted());
nfc_proxy_->StartReading(
this,
WTF::BindOnce(&NDEFReader::ReadOnRequestCompleted, WrapPersistent(this)));
}
void NDEFReader::ReadOnRequestCompleted(
device::mojom::blink::NDEFErrorPtr error) {
if (!scan_resolver_)
return;
ScriptState* script_state = scan_resolver_->GetScriptState();
if (!IsInParallelAlgorithmRunnable(scan_resolver_->GetExecutionContext(),
script_state)) {
scan_resolver_.Clear();
return;
}
ScriptState::Scope script_state_scope(script_state);
if (error) {
scan_resolver_->Reject(NDEFErrorPtrToDOMException(
script_state->GetIsolate(), std::move(error)));
} else {
scan_resolver_->Resolve();
}
scan_resolver_.Clear();
}
void NDEFReader::OnReading(const String& serial_number,
const device::mojom::blink::NDEFMessage& message) {
DCHECK(nfc_proxy_->IsReading(this));
DispatchEvent(*MakeGarbageCollected<NDEFReadingEvent>(
event_type_names::kReading, serial_number,
MakeGarbageCollected<NDEFMessage>(message)));
}
void NDEFReader::OnReadingError(const String& message) {
GetExecutionContext()->AddConsoleMessage(MakeGarbageCollected<ConsoleMessage>(
mojom::blink::ConsoleMessageSource::kJavaScript,
mojom::blink::ConsoleMessageLevel::kInfo, message));
// Dispatch the event as the final step in this method as it may cause script
// to run that destroys the execution context.
DispatchEvent(*Event::Create(event_type_names::kReadingerror));
}
void NDEFReader::ContextDestroyed() {
nfc_proxy_->StopReading(this);
scan_abort_handle_.Clear();
}
void NDEFReader::ReadAbort(AbortSignal* signal) {
nfc_proxy_->StopReading(this);
scan_abort_handle_.Clear();
if (!scan_resolver_)
return;
ScriptState* script_state = scan_resolver_->GetScriptState();
if (!IsInParallelAlgorithmRunnable(scan_resolver_->GetExecutionContext(),
script_state)) {
scan_resolver_.Clear();
return;
}
ScriptState::Scope script_state_scope(script_state);
scan_resolver_->Reject(scan_signal_->reason(script_state));
scan_resolver_.Clear();
}
// https://w3c.github.io/web-nfc/#writing-content
// https://w3c.github.io/web-nfc/#the-write-method
ScriptPromise NDEFReader::write(ScriptState* script_state,
const V8NDEFMessageSource* write_message,
const NDEFWriteOptions* options,
ExceptionState& exception_state) {
// https://w3c.github.io/web-nfc/#security-policies
// WebNFC API must be only accessible from top level browsing context.
if (!DomWindow() || !DomWindow()->GetFrame()->IsMainFrame()) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
kChildFrameErrorMessage);
return ScriptPromise();
}
std::unique_ptr<ScopedAbortState> scoped_abort_state = nullptr;
if (auto* signal = options->getSignalOr(nullptr)) {
if (signal->aborted()) {
return ScriptPromise::Reject(script_state, signal->reason(script_state));
}
auto* handle =
signal->AddAlgorithm(MakeGarbageCollected<WriteAbortAlgorithm>(this));
scoped_abort_state = std::make_unique<ScopedAbortState>(signal, handle);
}
// Step 11.2: Run "create NDEF message", if this throws an exception,
// reject p with that exception and abort these steps.
NDEFMessage* ndef_message =
NDEFMessage::Create(script_state, write_message, exception_state);
if (exception_state.HadException()) {
return ScriptPromise();
}
auto message = device::mojom::blink::NDEFMessage::From(ndef_message);
DCHECK(message);
auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(
script_state, exception_state.GetContext());
write_requests_.insert(resolver);
// Add the writer to proxy's writer list for Mojo connection error
// notification.
nfc_proxy_->AddWriter(this);
GetPermissionService()->RequestPermission(
CreatePermissionDescriptor(PermissionName::NFC),
LocalFrame::HasTransientUserActivation(DomWindow()->GetFrame()),
WTF::BindOnce(&NDEFReader::WriteOnRequestPermission, WrapPersistent(this),
WrapPersistent(resolver), std::move(scoped_abort_state),
WrapPersistent(options), std::move(message)));
return resolver->Promise();
}
void NDEFReader::WriteOnRequestPermission(
ScriptPromiseResolver* resolver,
std::unique_ptr<ScopedAbortState> scoped_abort_state,
const NDEFWriteOptions* options,
device::mojom::blink::NDEFMessagePtr message,
mojom::blink::PermissionStatus status) {
DCHECK(resolver);
ScriptState* script_state = resolver->GetScriptState();
if (!IsInParallelAlgorithmRunnable(resolver->GetExecutionContext(),
script_state)) {
return;
}
ScriptState::Scope script_state_scope(script_state);
if (status != mojom::blink::PermissionStatus::GRANTED) {
resolver->Reject(V8ThrowDOMException::CreateOrDie(
script_state->GetIsolate(), DOMExceptionCode::kNotAllowedError,
"NFC permission request denied."));
return;
}
AbortSignal* signal =
scoped_abort_state ? scoped_abort_state->Signal() : nullptr;
if (signal && signal->aborted()) {
resolver->Reject(signal->reason(script_state));
return;
}
auto callback =
WTF::BindOnce(&NDEFReader::WriteOnRequestCompleted, WrapPersistent(this),
WrapPersistent(resolver), std::move(scoped_abort_state));
nfc_proxy_->Push(std::move(message),
device::mojom::blink::NDEFWriteOptions::From(options),
std::move(callback));
}
void NDEFReader::WriteOnRequestCompleted(
ScriptPromiseResolver* resolver,
std::unique_ptr<ScopedAbortState> scoped_abort_state,
device::mojom::blink::NDEFErrorPtr error) {
DCHECK(write_requests_.Contains(resolver));
write_requests_.erase(resolver);
ScriptState* script_state = resolver->GetScriptState();
if (!IsInParallelAlgorithmRunnable(resolver->GetExecutionContext(),
script_state)) {
return;
}
AbortSignal* signal =
scoped_abort_state ? scoped_abort_state->Signal() : nullptr;
ScriptState::Scope script_state_scope(script_state);
if (error.is_null()) {
resolver->Resolve();
} else if (signal && signal->aborted()) {
resolver->Reject(signal->reason(script_state));
} else {
resolver->Reject(NDEFErrorPtrToDOMException(script_state->GetIsolate(),
std::move(error)));
}
}
void NDEFReader::WriteAbort() {
// WriteOnRequestCompleted() should always be called whether the push
// operation is cancelled successfully or not.
nfc_proxy_->CancelPush();
}
ScriptPromise NDEFReader::makeReadOnly(ScriptState* script_state,
const NDEFMakeReadOnlyOptions* options,
ExceptionState& exception_state) {
// https://w3c.github.io/web-nfc/#security-policies
// WebNFC API must be only accessible from top level browsing context.
if (!DomWindow() || !DomWindow()->GetFrame()->IsMainFrame()) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
kChildFrameErrorMessage);
return ScriptPromise();
}
std::unique_ptr<ScopedAbortState> scoped_abort_state = nullptr;
if (auto* signal = options->getSignalOr(nullptr)) {
if (signal->aborted()) {
return ScriptPromise::Reject(script_state, signal->reason(script_state));
}
auto* handle = signal->AddAlgorithm(
MakeGarbageCollected<MakeReadOnlyAbortAlgorithm>(this));
scoped_abort_state = std::make_unique<ScopedAbortState>(signal, handle);
}
auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(
script_state, exception_state.GetContext());
make_read_only_requests_.insert(resolver);
// Add the writer to proxy's writer list for Mojo connection error
// notification.
nfc_proxy_->AddWriter(this);
GetPermissionService()->RequestPermission(
CreatePermissionDescriptor(PermissionName::NFC),
LocalFrame::HasTransientUserActivation(DomWindow()->GetFrame()),
WTF::BindOnce(&NDEFReader::MakeReadOnlyOnRequestPermission,
WrapPersistent(this), WrapPersistent(resolver),
std::move(scoped_abort_state), WrapPersistent(options)));
return resolver->Promise();
}
void NDEFReader::MakeReadOnlyOnRequestPermission(
ScriptPromiseResolver* resolver,
std::unique_ptr<ScopedAbortState> scoped_abort_state,
const NDEFMakeReadOnlyOptions* options,
mojom::blink::PermissionStatus status) {
DCHECK(resolver);
ScriptState* script_state = resolver->GetScriptState();
if (!IsInParallelAlgorithmRunnable(resolver->GetExecutionContext(),
script_state)) {
return;
}
ScriptState::Scope script_state_scope(resolver->GetScriptState());
if (status != mojom::blink::PermissionStatus::GRANTED) {
resolver->Reject(V8ThrowDOMException::CreateOrDie(
script_state->GetIsolate(), DOMExceptionCode::kNotAllowedError,
"NFC permission request denied."));
return;
}
AbortSignal* signal =
scoped_abort_state ? scoped_abort_state->Signal() : nullptr;
if (signal && signal->aborted()) {
resolver->Reject(signal->reason(script_state));
return;
}
auto callback = WTF::BindOnce(&NDEFReader::MakeReadOnlyOnRequestCompleted,
WrapPersistent(this), WrapPersistent(resolver),
std::move(scoped_abort_state));
nfc_proxy_->MakeReadOnly(std::move(callback));
}
void NDEFReader::MakeReadOnlyOnRequestCompleted(
ScriptPromiseResolver* resolver,
std::unique_ptr<ScopedAbortState> scoped_abort_state,
device::mojom::blink::NDEFErrorPtr error) {
DCHECK(make_read_only_requests_.Contains(resolver));
make_read_only_requests_.erase(resolver);
ScriptState* script_state = resolver->GetScriptState();
if (!IsInParallelAlgorithmRunnable(resolver->GetExecutionContext(),
script_state)) {
return;
}
AbortSignal* signal =
scoped_abort_state ? scoped_abort_state->Signal() : nullptr;
ScriptState::Scope script_state_scope(script_state);
if (error.is_null()) {
resolver->Resolve();
} else if (signal && signal->aborted()) {
resolver->Reject(signal->reason(script_state));
} else {
resolver->Reject(NDEFErrorPtrToDOMException(script_state->GetIsolate(),
std::move(error)));
}
}
void NDEFReader::MakeReadOnlyAbort() {
// MakeReadOnlyOnRequestCompleted() should always be called whether the
// makeReadOnly operation is cancelled successfully or not.
nfc_proxy_->CancelMakeReadOnly();
}
void NDEFReader::Trace(Visitor* visitor) const {
visitor->Trace(nfc_proxy_);
visitor->Trace(permission_service_);
visitor->Trace(scan_resolver_);
visitor->Trace(scan_signal_);
visitor->Trace(scan_abort_handle_);
visitor->Trace(write_requests_);
visitor->Trace(make_read_only_requests_);
EventTargetWithInlineData::Trace(visitor);
ActiveScriptWrappable::Trace(visitor);
ExecutionContextLifecycleObserver::Trace(visitor);
}
PermissionService* NDEFReader::GetPermissionService() {
if (!permission_service_.is_bound()) {
ConnectToPermissionService(
GetExecutionContext(),
permission_service_.BindNewPipeAndPassReceiver(
GetExecutionContext()->GetTaskRunner(TaskType::kMiscPlatformAPI)));
}
return permission_service_.get();
}
void NDEFReader::ReadOnMojoConnectionError() {
// If |scan_resolver_| has already settled this rejection is silently ignored.
if (!scan_resolver_)
return;
ScriptState* script_state = scan_resolver_->GetScriptState();
if (!IsInParallelAlgorithmRunnable(scan_resolver_->GetExecutionContext(),
script_state)) {
scan_resolver_.Clear();
return;
}
ScriptState::Scope script_state_scope(script_state);
scan_resolver_->Reject(NDEFErrorTypeToDOMException(
script_state->GetIsolate(),
device::mojom::blink::NDEFErrorType::NOT_SUPPORTED,
kNotSupportedOrPermissionDenied));
scan_resolver_.Clear();
}
void NDEFReader::WriteOnMojoConnectionError() {
// If the mojo connection breaks, All push requests will be rejected with a
// default error.
// Script may execute during a call to Reject(). Swap these sets to prevent
// concurrent modification.
HeapHashSet<Member<ScriptPromiseResolver>> write_requests;
write_requests_.swap(write_requests);
for (ScriptPromiseResolver* resolver : write_requests) {
DCHECK(resolver);
ScriptState* script_state = resolver->GetScriptState();
if (!IsInParallelAlgorithmRunnable(resolver->GetExecutionContext(),
script_state)) {
continue;
}
ScriptState::Scope script_state_scope(script_state);
resolver->Reject(NDEFErrorTypeToDOMException(
script_state->GetIsolate(),
device::mojom::blink::NDEFErrorType::NOT_SUPPORTED,
kNotSupportedOrPermissionDenied));
}
}
void NDEFReader::MakeReadOnlyOnMojoConnectionError() {
// If the mojo connection breaks, All makeReadOnly requests will be rejected
// with a default error.
// Script may execute during a call to Reject(). Swap these sets to prevent
// concurrent modification.
HeapHashSet<Member<ScriptPromiseResolver>> make_read_only_requests;
make_read_only_requests_.swap(make_read_only_requests);
for (ScriptPromiseResolver* resolver : make_read_only_requests) {
DCHECK(resolver);
ScriptState* script_state = resolver->GetScriptState();
if (!IsInParallelAlgorithmRunnable(resolver->GetExecutionContext(),
script_state)) {
continue;
}
ScriptState::Scope script_state_scope(script_state);
resolver->Reject(NDEFErrorTypeToDOMException(
script_state->GetIsolate(),
device::mojom::blink::NDEFErrorType::NOT_SUPPORTED,
kNotSupportedOrPermissionDenied));
}
}
} // namespace blink