blob: bec07b0f1a12b05d0f0ccaeb1317cd5e672bc097 [file]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/webui/examples/renderer/render_frame_observer.h"
#include "base/check.h"
#include "base/functional/callback.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "components/guest_view/common/guest_view.mojom.h"
#include "components/guest_view/common/guest_view_constants.h"
#include "components/guest_view/renderer/guest_view_container.h"
#include "components/guest_view/renderer/guest_view_request.h"
#include "content/public/renderer/render_frame.h"
#include "content/public/renderer/render_frame_observer.h"
#include "content/public/renderer/render_thread.h"
#include "content/public/renderer/v8_value_converter.h"
#include "ipc/ipc_sync_channel.h"
#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h"
#include "third_party/blink/public/platform/scheduler/web_agent_group_scheduler.h"
#include "third_party/blink/public/web/web_custom_element.h"
#include "third_party/blink/public/web/web_document.h"
#include "third_party/blink/public/web/web_local_frame.h"
#include "v8/include/v8-context.h"
#include "v8/include/v8-external.h"
#include "v8/include/v8-function-callback.h"
#include "v8/include/v8-function.h"
#include "v8/include/v8-isolate.h"
#include "v8/include/v8-local-handle.h"
#include "v8/include/v8-primitive.h"
namespace webui_examples {
namespace {
class RenderFrameStatus final : public content::RenderFrameObserver {
public:
explicit RenderFrameStatus(content::RenderFrame* render_frame)
: content::RenderFrameObserver(render_frame) {}
~RenderFrameStatus() final = default;
bool IsRenderFrameAvailable() { return render_frame() != nullptr; }
// RenderFrameObserver implementation.
void OnDestruct() final {}
};
void AllowCustomElementNameRegistration(
const v8::FunctionCallbackInfo<v8::Value>& args) {
CHECK_EQ(args.Length(), 1);
CHECK(args[0]->IsFunction());
v8::Isolate* isolate = args.GetIsolate();
v8::HandleScope handle_scope(isolate);
v8::Local<v8::Context> context = isolate->GetCurrentContext();
v8::Local<v8::Function> callback = v8::Local<v8::Function>::Cast(args[0]);
blink::WebCustomElement::EmbedderNamesAllowedScope embedder_names_scope;
callback->Call(context, context->Global(), 0, nullptr).ToLocalChecked();
}
void GetNextId(const v8::FunctionCallbackInfo<v8::Value>& args) {
static int32_t current_id = 0;
args.GetReturnValue().Set(++current_id);
}
void RegisterWebView(const blink::LocalFrameToken& frame_token,
const v8::FunctionCallbackInfo<v8::Value>& args) {
CHECK(args.Length() == 1);
CHECK(args[0]->IsInt32());
int view_instance_id = args[0].As<v8::Int32>()->Value();
CHECK_NE(view_instance_id, guest_view::kInstanceIDNone);
// This view_holder is leaked. It likely should be bound to the returned
// object but views are not tracked.
auto* view_holder = new mojo::Remote<guest_view::mojom::ViewHandle>();
auto receiver = view_holder->BindNewPipeAndPassReceiver();
auto* web_frame = blink::WebLocalFrame::FromFrameToken(frame_token);
CHECK(web_frame);
auto* frame = content::RenderFrame::FromWebFrame(web_frame);
CHECK(frame);
mojo::AssociatedRemote<guest_view::mojom::GuestViewHost> guest_view;
frame->GetRemoteAssociatedInterfaces()->GetInterface(&guest_view);
guest_view->ViewCreated(view_instance_id, "BrowserWebView",
std::move(receiver));
args.GetReturnValue().SetUndefined();
}
content::RenderFrame* GetRenderFrame(v8::Local<v8::Value> value) {
v8::Local<v8::Context> context;
if (!v8::Local<v8::Object>::Cast(value)->GetCreationContext().ToLocal(
&context)) {
if (context.IsEmpty()) {
return nullptr;
}
}
blink::WebLocalFrame* frame = blink::WebLocalFrame::FrameForContext(context);
if (!frame) {
return nullptr;
}
return content::RenderFrame::FromWebFrame(frame);
}
void AttachIframeGuest(const v8::FunctionCallbackInfo<v8::Value>& args) {
// attachIframeGuest(
// containerId, guestInstanceId, attachParams, contentWindow)
CHECK_EQ(args.Length(), 4);
CHECK(args[0]->IsInt32());
CHECK(args[1]->IsInt32());
CHECK(args[2]->IsObject());
CHECK(args[3]->IsObject());
v8::Isolate* isolate = args.GetIsolate();
v8::HandleScope handle_scope(isolate);
v8::Local<v8::Context> context = isolate->GetCurrentContext();
int container_id = args[0].As<v8::Int32>()->Value();
int guest_instance_id = args[1].As<v8::Int32>()->Value();
// Getting the attach params could destroy the frame while it executes JS,
// so observe the render frame for destruction. We don't expect for this to
// occur in the Webshell, so we CHECK against it.
content::RenderFrame* render_frame = GetRenderFrame(args[3]);
RenderFrameStatus render_frame_status(render_frame);
std::unique_ptr<base::Value> attach_params =
content::V8ValueConverter::Create()->FromV8Value(args[2], context);
CHECK(attach_params);
CHECK(attach_params->is_dict());
CHECK(render_frame_status.IsRenderFrameAvailable());
blink::WebLocalFrame* frame = render_frame->GetWebFrame();
// Parent must exist.
blink::WebFrame* parent_frame = frame->Parent();
CHECK(parent_frame);
CHECK(parent_frame->IsWebLocalFrame());
content::RenderFrame* embedder_parent_frame =
content::RenderFrame::FromWebFrame(parent_frame->ToWebLocalFrame());
auto* guest_view_container =
guest_view::GuestViewContainer::FromID(container_id);
CHECK(!guest_view_container);
// Currently, the parent frame will always hold on to the WebView, which means
// the Webshell does not have to worry about cleanup upon a GC. The lifetime
// below will need to be reassessed if this assumption changes.
guest_view_container =
new guest_view::GuestViewContainer(embedder_parent_frame, container_id);
std::unique_ptr<guest_view::GuestViewAttachRequest> request =
std::make_unique<guest_view::GuestViewAttachRequest>(
guest_view_container, render_frame, guest_instance_id,
std::move(*attach_params).TakeDict(), v8::Local<v8::Function>(),
args.GetIsolate());
guest_view_container->IssueRequest(std::move(request));
args.GetReturnValue().SetUndefined();
}
// Helper to manage the various V8 required scopes and variables.
class V8BinderContext {
public:
using V8Callback =
base::RepeatingCallback<void(const v8::FunctionCallbackInfo<v8::Value>&)>;
explicit V8BinderContext(content::RenderFrame* render_frame)
: isolate_(
render_frame->GetWebFrame()->GetAgentGroupScheduler()->Isolate()),
handle_scope_(isolate_),
context_(render_frame->GetWebFrame()->MainWorldScriptContext()),
context_scope_(context_) {}
V8BinderContext(const V8BinderContext&) = delete;
const V8BinderContext& operator=(const V8BinderContext&) = delete;
void CreateWebshellObject() {
object_ = v8::Object::New(isolate_);
context_->Global()
->CreateDataProperty(context_, CreateV8String("webshell"), object_)
.FromJust();
}
void AddCallbackToWebshellObject(const char* name, V8Callback callback) {
v8::Local<v8::Object> callback_holder = v8::Object::New(isolate_);
v8::Global<v8::Object> global_callback_holder(isolate_, callback_holder);
std::unique_ptr<V8Callback> callback_container =
std::make_unique<V8Callback>(std::move(callback));
SetPrivateData(callback_holder, kCallback,
v8::External::New(isolate_, callback_container.get()));
global_callback_holder.SetWeak(callback_container.release(),
CleanupV8Callback,
v8::WeakCallbackType::kParameter);
object_
->CreateDataProperty(
context_, CreateV8String(name),
v8::Function::New(context_, CallCallback, callback_holder)
.ToLocalChecked())
.FromJust();
}
private:
static constexpr char kCallback[] = "callback";
void SetPrivateData(v8::Local<v8::Object> object,
const char* key,
v8::Local<v8::Value> value) {
object->SetPrivate(
context_, v8::Private::ForApi(isolate_, CreateV8String(key)), value);
}
static bool GetPrivateData(v8::Local<v8::Context> context,
v8::Local<v8::Object> object,
const char* key,
v8::Local<v8::Value>* value) {
v8::Isolate* isolate = context->GetIsolate();
return object
->GetPrivate(context,
v8::Private::ForApi(isolate, CreateV8String(isolate, key)))
.ToLocal(value);
}
v8::Local<v8::String> CreateV8String(const char* str) {
return CreateV8String(isolate_, str);
}
// static
static v8::Local<v8::String> CreateV8String(v8::Isolate* isolate,
const char* str) {
return v8::String::NewFromUtf8(isolate, str,
v8::NewStringType::kInternalized)
.ToLocalChecked();
}
static void CleanupV8Callback(const v8::WeakCallbackInfo<V8Callback>& data) {
std::unique_ptr<V8Callback> callback(data.GetParameter());
}
static void CallCallback(const v8::FunctionCallbackInfo<v8::Value>& args) {
v8::Isolate* isolate = args.GetIsolate();
v8::HandleScope handle_scope(isolate);
v8::Local<v8::Object> callback_holder = args.Data().As<v8::Object>();
v8::Local<v8::Context> context = isolate->GetCurrentContext();
v8::Local<v8::Value> callback_function_value;
if (isolate->IsExecutionTerminating()) {
return;
}
if (!GetPrivateData(context, callback_holder, kCallback,
&callback_function_value)) {
return;
}
CHECK(callback_function_value->IsExternal());
V8Callback* callback = static_cast<V8Callback*>(
callback_function_value.As<v8::External>()->Value());
callback->Run(args);
}
const raw_ptr<v8::Isolate> isolate_;
v8::HandleScope handle_scope_;
v8::Local<v8::Context> context_;
v8::Context::Scope context_scope_;
v8::Local<v8::Object> object_;
};
} // namespace
RenderFrameObserver::RenderFrameObserver(content::RenderFrame* render_frame)
: content::RenderFrameObserver(render_frame) {}
RenderFrameObserver::~RenderFrameObserver() = default;
void RenderFrameObserver::SelfOwn(
std::unique_ptr<RenderFrameObserver> this_instance) {
DCHECK_EQ(this, this_instance.get());
this_instance_ = std::move(this_instance);
}
void RenderFrameObserver::OnDestruct() {
this_instance_.reset();
}
void RenderFrameObserver::DidStartNavigation(
const GURL& url,
std::optional<blink::WebNavigationType> navigation_type) {
if (!url.SchemeIs("chrome") || url.host() != "browser") {
this_instance_.reset();
return;
}
}
void RenderFrameObserver::ReadyToCommitNavigation(
blink::WebDocumentLoader* document_loader) {
V8BinderContext binder_context(render_frame());
binder_context.CreateWebshellObject();
binder_context.AddCallbackToWebshellObject(
"allowWebviewElementRegistration",
base::BindRepeating(&AllowCustomElementNameRegistration));
binder_context.AddCallbackToWebshellObject("getNextId",
base::BindRepeating(&GetNextId));
binder_context.AddCallbackToWebshellObject(
"registerWebView",
base::BindRepeating(&RegisterWebView,
render_frame()->GetWebFrame()->GetLocalFrameToken()));
binder_context.AddCallbackToWebshellObject(
"attachIframeGuest", base::BindRepeating(&AttachIframeGuest));
}
} // namespace webui_examples