blob: 1c497c522d51c0e7ce5669e3f3cb5fcddcff2f18 [file] [log] [blame]
// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "extensions/renderer/api/messaging/gin_port.h"
#include <cstring>
#include <string_view>
#include "base/functional/bind.h"
#include "extensions/common/api/messaging/message.h"
#include "extensions/common/mojom/event_dispatcher.mojom.h"
#include "extensions/common/mojom/message_port.mojom.h"
#include "extensions/renderer/api/messaging/messaging_util.h"
#include "extensions/renderer/bindings/api_binding_util.h"
#include "extensions/renderer/bindings/api_event_handler.h"
#include "extensions/renderer/bindings/event_emitter.h"
#include "extensions/renderer/console.h"
#include "extensions/renderer/get_script_context.h"
#include "gin/arguments.h"
#include "gin/converter.h"
#include "gin/object_template_builder.h"
#include "v8/include/v8-context.h"
#include "v8/include/v8-cppgc.h"
#include "v8/include/v8-object.h"
#include "v8/include/v8-primitive.h"
namespace extensions {
namespace {
constexpr char kSenderKey[] = "sender";
constexpr char kOnMessageEvent[] = "onMessage";
constexpr char kOnDisconnectEvent[] = "onDisconnect";
constexpr char kContextInvalidatedError[] = "Extension context invalidated.";
} // namespace
GinPort::GinPort(v8::Local<v8::Context> context,
const PortId& port_id,
const std::string& name,
const mojom::ChannelType channel_type,
APIEventHandler* event_handler,
Delegate* delegate)
: port_id_(port_id),
name_(name),
channel_type_(channel_type),
event_handler_(event_handler),
delegate_(delegate),
accessed_sender_(false) {
context_invalidation_listener_.emplace(
context, base::BindOnce(&GinPort::OnContextInvalidated,
weak_factory_.GetWeakPtr()));
}
GinPort::~GinPort() = default;
gin::ObjectTemplateBuilder GinPort::GetObjectTemplateBuilder(
v8::Isolate* isolate) {
return gin::Wrappable<GinPort>::GetObjectTemplateBuilder(isolate)
.SetMethod("disconnect", &GinPort::DisconnectHandler)
.SetMethod("postMessage", &GinPort::PostMessageHandler)
.SetLazyDataProperty("name", &GinPort::GetName)
.SetLazyDataProperty("onDisconnect", &GinPort::GetOnDisconnectEvent)
.SetLazyDataProperty("onMessage", &GinPort::GetOnMessageEvent)
.SetLazyDataProperty("sender", &GinPort::GetSender);
}
const gin::WrapperInfo* GinPort::wrapper_info() const {
return &kWrapperInfo;
}
const char* GinPort::GetHumanReadableName() const {
return "Port";
}
void GinPort::DispatchOnMessage(v8::Local<v8::Context> context,
const Message& message) {
DCHECK_EQ(State::kActive, state_);
v8::Isolate* isolate = v8::Isolate::GetCurrent();
v8::HandleScope handle_scope(isolate);
v8::Context::Scope context_scope(context);
// Parsing should be fail-safe for kNative channel type as native messaging
// hosts can send malformed messages.
std::string error;
v8::Local<v8::Value> parsed_message = messaging_util::MessageToV8(
context, message, channel_type_ == mojom::ChannelType::kNative, &error);
v8::Local<v8::Object> self = GetWrapper(isolate).ToLocalChecked();
v8::LocalVector<v8::Value> args(isolate, {parsed_message, self});
if (error.empty()) {
DispatchEvent(context, &args, kOnMessageEvent);
} else {
ScriptContext* script_context = GetScriptContextFromV8Context(context);
console::AddMessage(script_context,
blink::mojom::ConsoleMessageLevel::kError, error);
}
}
void GinPort::DispatchOnDisconnect(v8::Local<v8::Context> context) {
DCHECK_EQ(State::kActive, state_);
// Update |state_| before dispatching the onDisconnect event, so that we are
// able to reject attempts to disconnect the port again or to send a message
// from the event handler.
state_ = State::kDisconnected;
v8::Isolate* isolate = v8::Isolate::GetCurrent();
v8::HandleScope handle_scope(isolate);
v8::Context::Scope context_scope(context);
v8::Local<v8::Object> self = GetWrapper(isolate).ToLocalChecked();
v8::LocalVector<v8::Value> args(isolate, {self});
DispatchEvent(context, &args, kOnDisconnectEvent);
InvalidateEvents(context);
DCHECK_NE(state_, State::kActive);
}
void GinPort::SetSender(v8::Local<v8::Context> context,
v8::Local<v8::Value> sender) {
DCHECK_EQ(State::kActive, state_);
DCHECK(!accessed_sender_)
<< "|sender| can only be set before its first access.";
v8::Isolate* isolate = v8::Isolate::GetCurrent();
v8::HandleScope handle_scope(isolate);
v8::Local<v8::Object> wrapper = GetWrapper(isolate).ToLocalChecked();
v8::Local<v8::Private> key =
v8::Private::ForApi(isolate, gin::StringToSymbol(isolate, kSenderKey));
v8::Maybe<bool> set_result = wrapper->SetPrivate(context, key, sender);
DCHECK(set_result.IsJust() && set_result.FromJust());
}
void GinPort::DisconnectHandler(gin::Arguments* arguments) {
if (state_ == State::kInvalidated) {
ThrowError(arguments->isolate(), kContextInvalidatedError);
return;
}
// NOTE: We don't currently throw an error for calling disconnect() multiple
// times, but we could.
if (state_ == State::kDisconnected) {
return;
}
v8::Local<v8::Context> context = arguments->GetHolderCreationContext();
InvalidateEvents(context);
delegate_->ClosePort(context, port_id_);
state_ = State::kDisconnected;
}
void GinPort::PostMessageHandler(gin::Arguments* arguments,
v8::Local<v8::Value> v8_message) {
v8::Isolate* isolate = arguments->isolate();
v8::Local<v8::Context> context = arguments->GetHolderCreationContext();
if (state_ == State::kInvalidated) {
ThrowError(isolate, kContextInvalidatedError);
return;
}
if (state_ == State::kDisconnected) {
ThrowError(isolate, "Attempting to use a disconnected port object");
return;
}
std::string error;
std::unique_ptr<Message> message = messaging_util::MessageFromV8(
context, v8_message, port_id_.serialization_format, &error);
// NOTE(devlin): JS-based bindings just log to the console here and return,
// rather than throwing an error. But it really seems like it should be an
// error. Let's see how this goes.
if (!message) {
ThrowError(isolate, error);
return;
}
delegate_->PostMessageToPort(context, port_id_, std::move(message));
}
std::string GinPort::GetName() {
return name_;
}
v8::Local<v8::Value> GinPort::GetOnDisconnectEvent(gin::Arguments* arguments) {
return GetEvent(arguments->GetHolderCreationContext(), kOnDisconnectEvent);
}
v8::Local<v8::Value> GinPort::GetOnMessageEvent(gin::Arguments* arguments) {
return GetEvent(arguments->GetHolderCreationContext(), kOnMessageEvent);
}
v8::Local<v8::Value> GinPort::GetSender(gin::Arguments* arguments) {
accessed_sender_ = true;
v8::Isolate* isolate = arguments->isolate();
v8::Local<v8::Object> wrapper = GetWrapper(isolate).ToLocalChecked();
v8::Local<v8::Private> key =
v8::Private::ForApi(isolate, gin::StringToSymbol(isolate, kSenderKey));
v8::Local<v8::Value> sender;
if (!wrapper->GetPrivate(arguments->GetHolderCreationContext(), key)
.ToLocal(&sender)) {
NOTREACHED();
}
return sender;
}
v8::Local<v8::Object> GinPort::GetEvent(v8::Local<v8::Context> context,
std::string_view event_name) {
DCHECK(event_name == kOnMessageEvent || event_name == kOnDisconnectEvent);
v8::Isolate* isolate = v8::Isolate::GetCurrent();
if (state_ == State::kInvalidated) {
ThrowError(isolate, kContextInvalidatedError);
return v8::Local<v8::Object>();
}
v8::Local<v8::Object> wrapper = GetWrapper(isolate).ToLocalChecked();
v8::Local<v8::Private> key =
v8::Private::ForApi(isolate, gin::StringToSymbol(isolate, event_name));
v8::Local<v8::Value> event_val;
if (!wrapper->GetPrivate(context, key).ToLocal(&event_val)) {
NOTREACHED();
}
DCHECK(!event_val.IsEmpty());
v8::Local<v8::Object> event_object;
if (event_val->IsUndefined()) {
event_object = event_handler_->CreateAnonymousEventInstance(context);
v8::Maybe<bool> set_result =
wrapper->SetPrivate(context, key, event_object);
if (!set_result.IsJust() || !set_result.FromJust()) {
NOTREACHED();
}
} else {
event_object = event_val.As<v8::Object>();
}
return event_object;
}
void GinPort::DispatchEvent(v8::Local<v8::Context> context,
v8::LocalVector<v8::Value>* args,
std::string_view event_name) {
v8::Isolate* isolate = v8::Isolate::GetCurrent();
v8::Local<v8::Value> on_message = GetEvent(context, event_name);
EventEmitter* emitter = nullptr;
gin::Converter<EventEmitter*>::FromV8(isolate, on_message, &emitter);
CHECK(emitter);
emitter->Fire(context, args, /*filter=*/nullptr,
/*on_dispatched_callback=*/v8::Local<v8::Function>(),
/*listener_error_callback=*/v8::Local<v8::Function>());
}
void GinPort::OnContextInvalidated() {
DCHECK_NE(state_, State::kInvalidated);
state_ = State::kInvalidated;
// Note: no need to InvalidateEvents() here, since the APIEventHandler will
// invalidate them when the context is disposed.
}
void GinPort::InvalidateEvents(v8::Local<v8::Context> context) {
// No need to invalidate the events if the context itself was already
// invalidated; the APIEventHandler will have already cleaned up the
// listeners.
if (state_ == State::kInvalidated) {
return;
}
// TODO(devlin): By calling GetEvent() here, we'll end up creating an event
// if one didn't exist. It would be more efficient to only invalidate events
// that the port has already created.
event_handler_->InvalidateCustomEvent(context,
GetEvent(context, kOnMessageEvent));
event_handler_->InvalidateCustomEvent(context,
GetEvent(context, kOnDisconnectEvent));
}
void GinPort::ThrowError(v8::Isolate* isolate, std::string_view error) {
isolate->ThrowException(
v8::Exception::Error(gin::StringToV8(isolate, error)));
}
} // namespace extensions