Commit d7ead40f authored by Albert Chaulk's avatar Albert Chaulk Committed by Commit Bot

Implement cast javascript channels for webviews

Cast javascript channels add a window.<foo>.postMessage(message)
method to selected RenderFrame instances to allow renderers to push
data to the embedder.

The usage looks like this:
Browser: add interface during init
Renderer: bind JsChannelBindingProvider, call Register, provide JsChannelClient
Browser: stick the pid/route/channel in a list

Embedder: AddJavascriptChannels
Browser: lookup current RenderFrame and corresponding channel, call
    JsChannelClient::CreateChannel, provide JsChannel
Renderer: create JS bindings

<script>: call function
Renderer: JsChannel::PostMessage
Browser: send message to embedder

Bug: b/141864193
Test: desktop cast_shell build + custom html
Change-Id: I1425cb115f679b876a8b9cb75c418b9d2a7a1506
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1863073Reviewed-by: default avatarDominick Ng <dominickn@chromium.org>
Reviewed-by: default avatarDaniel Nicoara <dnicoara@chromium.org>
Commit-Queue: Daniel Nicoara <dnicoara@chromium.org>
Cr-Commit-Position: refs/heads/master@{#708846}
parent f656c810
......@@ -387,6 +387,8 @@ cast_source_set("browser") {
"exo/cast_wm_helper.h",
"exo/wayland_server_controller.cc",
"exo/wayland_server_controller.h",
"webview/js_channel_service.cc",
"webview/js_channel_service.h",
"webview/platform_views_rpc_instance.cc",
"webview/platform_views_rpc_instance.h",
"webview/web_content_controller.cc",
......
......@@ -137,7 +137,9 @@
#endif
#if BUILDFLAG(ENABLE_CAST_WAYLAND_SERVER)
#include "chromecast/browser/webview/js_channel_service.h"
#include "chromecast/browser/webview/webview_controller.h"
#include "chromecast/common/mojom/js_channel.mojom.h"
#endif // BUILDFLAG(ENABLE_CAST_WAYLAND_SERVER)
namespace chromecast {
......@@ -1020,6 +1022,20 @@ bool CastContentBrowserClient::DoesSiteRequireDedicatedProcess(
#endif
}
void CastContentBrowserClient::BindHostReceiverForRenderer(
content::RenderProcessHost* render_process_host,
mojo::GenericPendingReceiver receiver) {
#if BUILDFLAG(ENABLE_CAST_WAYLAND_SERVER)
if (auto r = receiver.As<::chromecast::mojom::JsChannelBindingProvider>()) {
JsChannelService::Create(render_process_host, std::move(r),
base::ThreadTaskRunnerHandle::Get());
return;
}
#endif
ContentBrowserClient::BindHostReceiverForRenderer(render_process_host,
std::move(receiver));
}
std::string CastContentBrowserClient::GetUserAgent() {
return chromecast::shell::GetUserAgent();
}
......
......@@ -226,6 +226,9 @@ class CastContentBrowserClient
std::string GetUserAgent() override;
bool DoesSiteRequireDedicatedProcess(content::BrowserContext* browser_context,
const GURL& effective_site_url) override;
void BindHostReceiverForRenderer(
content::RenderProcessHost* render_process_host,
mojo::GenericPendingReceiver receiver) override;
CastFeatureListCreator* GetCastFeatureListCreator() {
return cast_feature_list_creator_;
}
......
// Copyright 2019 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 "chromecast/browser/webview/js_channel_service.h"
#include <algorithm>
#include "base/lazy_instance.h"
#include "content/public/browser/render_process_host.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
namespace chromecast {
namespace {
class JsChannelImpl : public mojom::JsChannel {
public:
JsChannelImpl(const std::string& channel, JsChannelCallback callback);
~JsChannelImpl() override;
private:
void PostMessage(const std::string& message) override;
std::string channel_;
JsChannelCallback callback_;
DISALLOW_COPY_AND_ASSIGN(JsChannelImpl);
};
struct Instance {
int process_id;
int routing_id;
JsClientInstance* instance;
};
using EndpointList = std::vector<Instance>;
static base::LazyInstance<EndpointList>::DestructorAtExit g_instance_list =
LAZY_INSTANCE_INITIALIZER;
} // namespace
JsChannelService::JsChannelService(int process_id) : process_id_(process_id) {}
JsChannelService::~JsChannelService() = default;
// static
void JsChannelService::Create(
content::RenderProcessHost* render_process_host,
mojo::PendingReceiver<mojom::JsChannelBindingProvider> receiver,
scoped_refptr<base::SingleThreadTaskRunner> task_runner) {
mojo::MakeSelfOwnedReceiver(
std::make_unique<JsChannelService>(render_process_host->GetID()),
std::move(receiver), std::move(task_runner));
}
void JsChannelService::Register(
int routing_id,
mojo::PendingRemote<mojom::JsChannelClient> client) {
if (JsClientInstance::Find(process_id_, routing_id)) {
LOG(ERROR) << "Duplicate process/routing ID pair!";
} else {
// ClientInstance's lifetime is tied to the JsChannelClient mojo
// channel. It will delete itself when that channel closes.
new JsClientInstance(process_id_, routing_id, std::move(client));
}
}
JsClientInstance::JsClientInstance(
int process_id,
int routing_id,
mojo::PendingRemote<mojom::JsChannelClient> client)
: client_(std::move(client)) {
client_.set_disconnect_with_reason_handler(
base::BindRepeating([](JsClientInstance* self, uint32_t err,
const std::string& str) { delete self; },
base::Unretained(this)));
g_instance_list.Get().push_back({process_id, routing_id, this});
}
JsClientInstance::~JsClientInstance() {
auto& list = g_instance_list.Get();
list.erase(std::find_if(list.begin(), list.end(), [this](const auto& e) {
return e.instance == this;
}));
}
// static
JsClientInstance* JsClientInstance::Find(int process_id, int routing_id) {
for (auto& e : g_instance_list.Get()) {
if (e.process_id == process_id && e.routing_id == routing_id)
return e.instance;
}
return nullptr;
}
void JsClientInstance::AddChannel(const std::string& channel,
JsChannelCallback callback) {
mojo::PendingRemote<mojom::JsChannel> channel_remote;
auto receiver = channel_remote.InitWithNewPipeAndPassReceiver();
client_->CreateChannel(channel, std::move(channel_remote));
mojo::MakeSelfOwnedReceiver(
std::make_unique<JsChannelImpl>(channel, std::move(callback)),
std::move(receiver));
}
void JsClientInstance::RemoveChannel(const std::string& channel) {
client_->RemoveChannel(channel);
}
JsChannelImpl::JsChannelImpl(const std::string& channel,
JsChannelCallback callback)
: channel_(channel), callback_(std::move(callback)) {}
JsChannelImpl::~JsChannelImpl() = default;
void JsChannelImpl::PostMessage(const std::string& message) {
callback_.Run(channel_, message);
}
} // namespace chromecast
// Copyright 2019 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.
#ifndef CHROMECAST_BROWSER_WEBVIEW_JS_CHANNEL_SERVICE_H_
#define CHROMECAST_BROWSER_WEBVIEW_JS_CHANNEL_SERVICE_H_
#include "base/callback_forward.h"
#include "base/macros.h"
#include "chromecast/common/mojom/js_channel.mojom.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/unique_receiver_set.h"
#include "services/service_manager/public/cpp/binder_registry.h"
namespace content {
class RenderProcessHost;
} // namespace content
namespace chromecast {
using JsChannelCallback =
base::RepeatingCallback<void(const std::string&, const std::string&)>;
class JsChannelService : public mojom::JsChannelBindingProvider {
public:
static void Create(
content::RenderProcessHost* render_process_host,
mojo::PendingReceiver<mojom::JsChannelBindingProvider> receiver,
scoped_refptr<base::SingleThreadTaskRunner> task_runner);
explicit JsChannelService(int process_id);
~JsChannelService() override;
private:
// mojom::JsChannelBindingProvider implementation:
void Register(int routing_id,
mojo::PendingRemote<mojom::JsChannelClient> client) override;
int process_id_;
DISALLOW_COPY_AND_ASSIGN(JsChannelService);
};
class JsClientInstance {
public:
void AddChannel(const std::string& channel, JsChannelCallback callback);
void RemoveChannel(const std::string& channel);
static JsClientInstance* Find(int process_id, int routing_id);
private:
friend class JsChannelService;
// These are created by JsChannelService.
JsClientInstance(int process_id,
int routing_id,
mojo::PendingRemote<mojom::JsChannelClient> client);
~JsClientInstance();
mojo::Remote<mojom::JsChannelClient> client_;
mojo::UniqueReceiverSet<mojom::JsChannel> channels_;
DISALLOW_COPY_AND_ASSIGN(JsClientInstance);
};
} // namespace chromecast
#endif // CHROMECAST_BROWSER_WEBVIEW_JS_CHANNEL_SERVICE_H_
......@@ -13,6 +13,7 @@
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browsing_data_remover.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/render_widget_host.h"
#include "content/public/browser/render_widget_host_view.h"
......@@ -24,7 +25,9 @@
namespace chromecast {
WebContentController::WebContentController(Client* client) : client_(client) {}
WebContentController::WebContentController(Client* client) : client_(client) {
js_channels_ = std::make_unique<WebContentJsChannels>(client_);
}
WebContentController::~WebContentController() {
if (surface_) {
......@@ -251,12 +254,42 @@ void WebContentController::HandleEvaluateJavascript(
void WebContentController::HandleAddJavascriptChannels(
const webview::AddJavascriptChannelsRequest& request) {
// TODO(dnicoara): Handle this.
auto* rfh = GetWebContents()->GetMainFrame();
if (!rfh) {
LOG(WARNING) << "No current RenderFrameHost";
return;
}
auto* client =
JsClientInstance::Find(rfh->GetProcess()->GetID(), rfh->GetRoutingID());
if (!client) {
LOG(WARNING) << "Requested to add JS channels but no mojo client found";
return;
}
for (auto& channel : request.channels()) {
client->AddChannel(channel,
base::BindRepeating(&WebContentJsChannels::SendMessage,
js_channels_->AsWeakPtr()));
}
}
void WebContentController::HandleRemoveJavascriptChannels(
const webview::RemoveJavascriptChannelsRequest& request) {
// TODO(dnicoara): Handle this.
auto* rfh = GetWebContents()->GetMainFrame();
if (!rfh)
return;
auto* client =
JsClientInstance::Find(rfh->GetProcess()->GetID(), rfh->GetRoutingID());
if (!client) {
LOG(WARNING) << "Requested to remove JS channels but no mojo client found";
return;
}
for (auto& channel : request.channels()) {
client->RemoveChannel(channel);
}
}
void WebContentController::HandleGetCurrentUrl(int64_t id) {
......@@ -362,4 +395,19 @@ void WebContentController::OnSurfaceDestroying(exo::Surface* surface) {
surface_ = nullptr;
}
WebContentJsChannels::WebContentJsChannels(WebContentController::Client* client)
: client_(client) {}
WebContentJsChannels::~WebContentJsChannels() = default;
void WebContentJsChannels::SendMessage(const std::string& channel,
const std::string& message) {
std::unique_ptr<webview::WebviewResponse> response =
std::make_unique<webview::WebviewResponse>();
auto* js_message = response->mutable_javascript_channel_message();
js_message->set_channel(channel);
js_message->set_message(message);
client_->EnqueueSend(std::move(response));
}
} // namespace chromecast
......@@ -8,6 +8,7 @@
#include <memory>
#include <string>
#include "chromecast/browser/webview/js_channel_service.h"
#include "chromecast/browser/webview/proto/webview.pb.h"
#include "components/exo/surface.h"
#include "components/exo/surface_observer.h"
......@@ -23,6 +24,8 @@ class WebContents;
namespace chromecast {
class WebContentJsChannels;
// Processes proto commands to control WebContents
class WebContentController : public exo::SurfaceObserver {
public:
......@@ -47,6 +50,7 @@ class WebContentController : public exo::SurfaceObserver {
virtual content::WebContents* GetWebContents() = 0;
Client* client_; // Not owned.
bool has_navigation_delegate_ = false;
std::unique_ptr<WebContentJsChannels> js_channels_;
private:
void ProcessInputEvent(const webview::InputEvent& ev);
......@@ -80,6 +84,20 @@ class WebContentController : public exo::SurfaceObserver {
DISALLOW_COPY_AND_ASSIGN(WebContentController);
};
class WebContentJsChannels
: public base::SupportsWeakPtr<WebContentJsChannels> {
public:
explicit WebContentJsChannels(WebContentController::Client* client);
~WebContentJsChannels();
void SendMessage(const std::string& channel, const std::string& message);
private:
WebContentController::Client* client_;
DISALLOW_COPY_AND_ASSIGN(WebContentJsChannels);
};
} // namespace chromecast
#endif // CHROMECAST_BROWSER_WEBVIEW_WEB_CONTENT_CONTROLLER_H_
......@@ -4,7 +4,10 @@
#include "chromecast/browser/webview/webview_controller.h"
#include <set>
#include "base/json/json_writer.h"
#include "base/memory/weak_ptr.h"
#include "base/strings/utf_string_conversions.h"
#include "chromecast/base/version.h"
#include "chromecast/browser/cast_web_contents_impl.h"
......@@ -14,6 +17,7 @@
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browsing_data_remover.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/render_widget_host.h"
#include "content/public/browser/render_widget_host_view.h"
......@@ -186,6 +190,7 @@ void WebviewController::ResourceLoadFailed(CastWebContents* cast_web_contents) {
void WebviewController::Destroy() {
// This webview is now abandoned and should close.
client_ = nullptr;
js_channels_.reset();
if (stopped_) {
// If the page has been stopped this can be deleted immediately.
delete this;
......
......@@ -9,6 +9,7 @@ mojom("mojom") {
"application_media_capabilities.mojom",
"constants.mojom",
"feature_manager.mojom",
"js_channel.mojom",
"media_caps.mojom",
"media_playback_options.mojom",
"memory_pressure.mojom",
......
// Copyright 2019 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.
module chromecast.mojom;
// Send a message on this channel to the browser.
interface JsChannel {
// Message from javascript.
PostMessage(string message);
};
// Implemented on the renderer, this gets notified by the browser to
// add/remove channels as needed.
interface JsChannelClient {
// Add a new channel binding, connecting window.channel.postMessage to |pipe|.
CreateChannel(string channel, pending_remote<JsChannel> pipe);
// Remove any existing channel binding.
RemoveChannel(string channel);
};
// Implemented by the browser.
interface JsChannelBindingProvider {
// The RenderFrame reports its routing ID to the browser here and provides the
// pipe to manage channels.
Register(int32 routing_id, pending_remote<JsChannelClient> client);
};
......@@ -32,6 +32,8 @@ cast_source_set("renderer") {
"cast_media_playback_options.h",
"cast_url_loader_throttle_provider.cc",
"cast_url_loader_throttle_provider.h",
"js_channel_bindings.cc",
"js_channel_bindings.h",
"native_bindings_helper.cc",
"native_bindings_helper.h",
"on_load_script_injector.cc",
......
......@@ -17,6 +17,7 @@
#include "chromecast/public/media/media_capabilities_shlib.h"
#include "chromecast/renderer/cast_media_playback_options.h"
#include "chromecast/renderer/cast_url_loader_throttle_provider.h"
#include "chromecast/renderer/js_channel_bindings.h"
#include "chromecast/renderer/media/key_systems_cast.h"
#include "chromecast/renderer/media/media_caps_observer_impl.h"
#include "chromecast/renderer/on_load_script_injector.h"
......@@ -194,6 +195,11 @@ void CastContentRendererClient::RenderFrameCreated(
dispatcher->OnRenderFrameCreated(render_frame);
#endif
#if BUILDFLAG(ENABLE_CAST_WAYLAND_SERVER)
// JsChannelBindings destroys itself when the RenderFrame is destroyed.
JsChannelBindings::Create(render_frame);
#endif
}
content::BrowserPluginDelegate*
......
// Copyright 2019 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 "chromecast/renderer/js_channel_bindings.h"
#include "chromecast/renderer/native_bindings_helper.h"
#include "content/public/renderer/render_frame.h"
#include "content/public/renderer/render_thread.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "third_party/blink/public/web/blink.h"
#include "third_party/blink/public/web/web_local_frame.h"
namespace chromecast {
// These are defined to be a set of objects that provide a postMessage(string)
// method. In turn they forward messages over their mojo channel to the browser.
JsChannelBindings::JsChannelBindings(
content::RenderFrame* render_frame,
mojo::PendingReceiver<mojom::JsChannelClient> receiver)
: content::RenderFrameObserver(render_frame),
receiver_(this, std::move(receiver)) {}
JsChannelBindings::~JsChannelBindings() {
while (!channels_.empty())
RemoveChannel(channels_.back().first);
}
void JsChannelBindings::Create(content::RenderFrame* render_frame) {
content::RenderThread* render_thread = content::RenderThread::Get();
// First, get a connection to the main service for our process.
mojo::PendingRemote<mojom::JsChannelBindingProvider> pending_remote;
render_thread->BindHostReceiver(
pending_remote.InitWithNewPipeAndPassReceiver());
mojo::Remote<mojom::JsChannelBindingProvider> provider(
std::move(pending_remote));
mojo::PendingRemote<mojom::JsChannelClient> client;
// This deletes itself when the RenderFrame is destroyed.
new JsChannelBindings(render_frame, client.InitWithNewPipeAndPassReceiver());
// Tell the browser that we are ready to receive pipes.
provider->Register(render_frame->GetRoutingID(), std::move(client));
}
void JsChannelBindings::DidClearWindowObject() {
for (auto& e : channels_)
Install(e.first);
}
void JsChannelBindings::OnDestruct() {
delete this;
}
void JsChannelBindings::Install(const std::string& channel) {
blink::WebLocalFrame* web_frame = render_frame()->GetWebFrame();
if (!web_frame)
return;
v8::Isolate* isolate = blink::MainThreadIsolate();
if (!isolate)
return;
v8::HandleScope handle_scope(isolate);
v8::Local<v8::Context> context = web_frame->MainWorldScriptContext();
if (context.IsEmpty())
return;
v8::Context::Scope context_scope(context);
auto container = EnsureObjectExists(isolate, context->Global(), channel);
InstallBinding(isolate, container, "postMessage", &JsChannelBindings::Func,
base::Unretained(this), channel);
}
void JsChannelBindings::Func(const std::string& channel,
v8::Local<v8::Value> message) {
for (auto& e : channels_) {
if (e.first == channel) {
v8::String::Utf8Value utf8(blink::MainThreadIsolate(), message);
e.second->PostMessage(*utf8);
break;
}
}
}
void JsChannelBindings::CreateChannel(
const std::string& channel,
mojo::PendingRemote<mojom::JsChannel> pipe) {
channels_.push_back(
std::make_pair(channel, mojo::Remote<mojom::JsChannel>(std::move(pipe))));
Install(channel);
}
void JsChannelBindings::RemoveChannel(const std::string& channel) {
for (auto iter = channels_.begin(); iter != channels_.end(); ++iter) {
if (iter->first == channel) {
channels_.erase(iter);
break;
}
}
// Remove V8 object.
blink::WebLocalFrame* web_frame = render_frame()->GetWebFrame();
if (!web_frame)
return;
v8::Isolate* isolate = blink::MainThreadIsolate();
if (!isolate)
return;
v8::HandleScope handle_scope(isolate);
v8::Local<v8::Context> context = web_frame->MainWorldScriptContext();
if (context.IsEmpty())
return;
v8::Context::Scope context_scope(context);
auto result = context->Global()->Set(
context, gin::StringToSymbol(isolate, channel), v8::Local<v8::Object>());
if (result.IsNothing() || !result.FromJust())
VLOG(1) << "Failed to remove binding for method " << channel;
}
} // namespace chromecast
// Copyright 2019 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.
#ifndef CHROMECAST_RENDERER_JS_CHANNEL_BINDINGS_H_
#define CHROMECAST_RENDERER_JS_CHANNEL_BINDINGS_H_
#include "base/macros.h"
#include "chromecast/common/mojom/js_channel.mojom.h"
#include "content/public/renderer/render_frame_observer.h"
namespace chromecast {
class JsChannelBindings : public content::RenderFrameObserver,
public mojom::JsChannelClient {
public:
static void Create(content::RenderFrame* render_frame);
explicit JsChannelBindings(
content::RenderFrame* render_frame,
mojo::PendingReceiver<mojom::JsChannelClient> receiver);
~JsChannelBindings() override;
private:
// content::RenderFrameObserver implementation:
void DidClearWindowObject() final;
void OnDestruct() final;
// mojom::JsChannelClient implementation:
void CreateChannel(const std::string& channel,
mojo::PendingRemote<mojom::JsChannel> pipe) override;
void RemoveChannel(const std::string& channel) override;
void Install(const std::string& channel);
void Func(const std::string& channel, v8::Local<v8::Value> message);
std::vector<std::pair<std::string, mojo::Remote<mojom::JsChannel>>> channels_;
mojo::Receiver<mojom::JsChannelClient> receiver_;
DISALLOW_COPY_AND_ASSIGN(JsChannelBindings);
};
} // namespace chromecast
#endif // CHROMECAST_RENDERER_JS_CHANNEL_BINDINGS_H_
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment