blob: 3dd6792ccac367b741bc8a469cf2e18a1f395404 [file] [log] [blame]
// Copyright 2022 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/browser/devtools/devtools_frontend.h"
#include <map>
#include <memory>
#include <string_view>
#include "base/memory/raw_ptr.h"
#include "base/memory/ref_counted.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/uuid.h"
#include "base/values.h"
#include "content/public/browser/devtools_agent_host.h"
#include "content/public/browser/devtools_frontend_host.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/browser/web_contents_user_data.h"
#include "ipc/constants.mojom.h"
#include "ui/webui/examples/browser/devtools/devtools_server.h"
#include "url/gurl.h"
namespace webui_examples {
namespace {
static GURL GetFrontendURL() {
return GURL(
base::StringPrintf("http://127.0.0.1:%d/devtools/devtools_app.html",
devtools::GetHttpHandlerPort()));
}
// This constant should be in sync with
// the constant
// kMaxMessageChunkSize in chrome/browser/devtools/devtools_ui_bindings.cc.
constexpr size_t kMaxMessageChunkSize =
IPC::mojom::kChannelMaximumMessageSize / 4;
} // namespace
class DevToolsFrontend::AgentHostClient
: public content::WebContentsObserver,
public content::DevToolsAgentHostClient {
public:
AgentHostClient(content::WebContents* devtools_contents,
content::WebContents* inspected_contents)
: content::WebContentsObserver(devtools_contents),
devtools_contents_(devtools_contents),
inspected_contents_(inspected_contents) {}
AgentHostClient(const AgentHostClient&) = delete;
AgentHostClient& operator=(const AgentHostClient&) = delete;
~AgentHostClient() override = default;
// content::DevToolsAgentHostClient
void DispatchProtocolMessage(content::DevToolsAgentHost* agent_host,
base::span<const uint8_t> message) override {
std::string_view str_message(reinterpret_cast<const char*>(message.data()),
message.size());
if (str_message.length() < kMaxMessageChunkSize) {
CallClientFunction("DevToolsAPI", "dispatchMessage",
base::Value(std::string(str_message)));
} else {
size_t total_size = str_message.length();
for (size_t pos = 0; pos < str_message.length();
pos += kMaxMessageChunkSize) {
std::string_view str_message_chunk =
str_message.substr(pos, kMaxMessageChunkSize);
CallClientFunction(
"DevToolsAPI", "dispatchMessageChunk",
base::Value(std::string(str_message_chunk)),
base::Value(base::NumberToString(pos ? 0 : total_size)));
}
}
}
void AgentHostClosed(content::DevToolsAgentHost* agent_host) override {}
void FrameDeleted(content::FrameTreeNodeId frame_tree_node_id) override {
if (agent_host_) {
agent_host_->DetachClient(this);
agent_host_.reset();
}
}
void Attach() {
if (agent_host_)
agent_host_->DetachClient(this);
agent_host_ =
content::DevToolsAgentHost::GetOrCreateFor(inspected_contents_);
agent_host_->AttachClient(this);
}
void CallClientFunction(
const std::string& object_name,
const std::string& method_name,
base::Value arg1 = {},
base::Value arg2 = {},
base::Value arg3 = {},
base::OnceCallback<void(base::Value)> cb = base::NullCallback()) {
content::RenderFrameHost* host = devtools_contents_->GetPrimaryMainFrame();
host->AllowInjectingJavaScript();
base::ListValue arguments;
if (!arg1.is_none()) {
arguments.Append(std::move(arg1));
if (!arg2.is_none()) {
arguments.Append(std::move(arg2));
if (!arg3.is_none()) {
arguments.Append(std::move(arg3));
}
}
}
host->ExecuteJavaScriptMethod(base::ASCIIToUTF16(object_name),
base::ASCIIToUTF16(method_name),
std::move(arguments), std::move(cb));
}
private:
// content::WebContentsObserver:
void ReadyToCommitNavigation(
content::NavigationHandle* navigation_handle) override {
content::RenderFrameHost* frame = navigation_handle->GetRenderFrameHost();
// TODO(crbug.com/40185886): With MPArch there may be multiple main
// frames. This caller was converted automatically to the primary main frame
// to preserve its semantics. Follow up to confirm correctness.
if (navigation_handle->IsInPrimaryMainFrame()) {
frontend_host_ = content::DevToolsFrontendHost::Create(
frame, base::BindRepeating(
&AgentHostClient::HandleMessageFromDevToolsFrontend,
base::Unretained(this)));
return;
}
std::string origin =
navigation_handle->GetURL().DeprecatedGetOriginAsURL().spec();
auto it = extensions_api_.find(origin);
if (it == extensions_api_.end())
return;
std::string script = base::StringPrintf(
"%s(\"%s\")", it->second.c_str(),
base::Uuid::GenerateRandomV4().AsLowercaseString().c_str());
content::DevToolsFrontendHost::SetupExtensionsAPI(frame, script);
}
void HandleMessageFromDevToolsFrontend(base::DictValue message) {
const std::string* method = message.FindString("method");
if (!method)
return;
int request_id = message.FindInt("id").value_or(0);
base::ListValue* params_value = message.FindList("params");
// Since we've received message by value, we can take the list.
base::ListValue params;
if (params_value) {
params = std::move(*params_value);
}
if (*method == "dispatchProtocolMessage" && params.size() == 1) {
const std::string* protocol_message = params[0].GetIfString();
if (!agent_host_ || !protocol_message)
return;
agent_host_->DispatchProtocolMessage(
this, base::as_byte_span(*protocol_message));
} else if (*method == "loadCompleted") {
CallClientFunction("DevToolsAPI", "setUseSoftMenu", base::Value(true));
} else if (*method == "loadNetworkResource" && params.size() == 3) {
// TODO(robliao): Add support for this if necessary.
NOTREACHED();
} else if (*method == "getPreferences") {
SendMessageAck(request_id, base::Value(std::move(preferences_)));
return;
} else if (*method == "getHostConfig") {
base::DictValue response_dict;
// Chrome's DevToolsUIBindings sets feature flag values to this
// devToolsVeLogging dictionary, but they're not accessible from //ui.
// Just set the default values instead.
base::DictValue ve_logging_dict;
ve_logging_dict.Set("enabled", true);
ve_logging_dict.Set("testing", false);
response_dict.Set("devToolsVeLogging", std::move(ve_logging_dict));
SendMessageAck(request_id, base::Value(std::move(response_dict)));
return;
} else if (*method == "setPreference") {
if (params.size() < 2)
return;
const std::string* name = params[0].GetIfString();
// We're just setting params[1] as a value anyways, so just make sure it's
// the type we want, but don't worry about getting it.
if (!name || !params[1].is_string())
return;
preferences_.Set(*name, std::move(params[1]));
} else if (*method == "removePreference") {
const std::string* name = params[0].GetIfString();
if (!name)
return;
preferences_.Remove(*name);
} else if (*method == "requestFileSystems") {
CallClientFunction("DevToolsAPI", "fileSystemsLoaded",
base::Value(base::Value::Type::LIST));
} else if (*method == "reattach") {
if (!agent_host_)
return;
agent_host_->DetachClient(this);
agent_host_->AttachClient(this);
} else if (*method == "registerExtensionsAPI") {
if (params.size() < 2)
return;
const std::string* origin = params[0].GetIfString();
const std::string* script = params[1].GetIfString();
if (!origin || !script)
return;
extensions_api_[*origin + "/"] = *script;
} else {
return;
}
if (request_id)
SendMessageAck(request_id, {});
}
void SendMessageAck(int request_id, base::Value arg) {
CallClientFunction("DevToolsAPI", "embedderMessageAck",
base::Value(request_id), std::move(arg));
}
const raw_ptr<content::WebContents> devtools_contents_;
const raw_ptr<content::WebContents> inspected_contents_;
scoped_refptr<content::DevToolsAgentHost> agent_host_;
std::unique_ptr<content::DevToolsFrontendHost> frontend_host_;
std::map<std::string, std::string> extensions_api_;
base::DictValue preferences_;
};
class DevToolsFrontend::Pointer : public content::WebContentsUserData<Pointer> {
public:
~Pointer() override = default;
static DevToolsFrontend* Create(content::WebContents* web_contents) {
CreateForWebContents(web_contents);
Pointer* ptr = FromWebContents(web_contents);
return ptr->Get();
}
DevToolsFrontend* Get() { return ptr_.get(); }
private:
friend class content::WebContentsUserData<Pointer>;
Pointer(content::WebContents* web_contents)
: content::WebContentsUserData<Pointer>(*web_contents),
ptr_(new DevToolsFrontend(web_contents)) {}
Pointer(const Pointer*) = delete;
Pointer& operator=(const Pointer&) = delete;
WEB_CONTENTS_USER_DATA_KEY_DECL();
std::unique_ptr<DevToolsFrontend> ptr_;
};
WEB_CONTENTS_USER_DATA_KEY_IMPL(DevToolsFrontend::Pointer);
DevToolsFrontend::DevToolsFrontend(content::WebContents* inspected_contents)
: frontend_url_(GetFrontendURL()),
inspected_contents_(inspected_contents) {}
DevToolsFrontend::~DevToolsFrontend() = default;
// static
DevToolsFrontend* DevToolsFrontend::CreateAndGet(
content::WebContents* inspected_contents) {
return DevToolsFrontend::Pointer::Create(inspected_contents);
}
void DevToolsFrontend::SetDevtoolsWebContents(
content::WebContents* devtools_contents) {
devtools_contents_ = devtools_contents;
agent_host_client_ = std::make_unique<AgentHostClient>(devtools_contents_,
inspected_contents_);
agent_host_client_->Attach();
}
} // namespace webui_examples