910e62b5创建于 1月15日历史提交
// 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 "content/web_test/browser/devtools_protocol_test_bindings.h"

#include <string_view>

#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/json/string_escape.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "base/values.h"
#include "build/build_config.h"
#include "content/public/browser/devtools_agent_host.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/isolated_world_ids.h"
#include "content/web_test/browser/web_test_control_host.h"
#include "content/web_test/common/web_test_switches.h"
#include "ipc/constants.mojom.h"

#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS) && !BUILDFLAG(IS_FUCHSIA)
#include "content/public/browser/devtools_frontend_host.h"
#endif

namespace content {

namespace {
// This constant should be in sync with
// the constant
// kMaxMessageChunkSize in chrome/browser/devtools/devtools_ui_bindings.cc.
constexpr size_t kWebTestMaxMessageChunkSize =
    IPC::mojom::kChannelMaximumMessageSize / 4;
}  // namespace

DevToolsProtocolTestBindings::DevToolsProtocolTestBindings(
    WebContents* devtools,
    std::string log)
    : WebContentsObserver(devtools),
      agent_host_(DevToolsAgentHost::CreateForBrowser(
          nullptr,
          DevToolsAgentHost::CreateServerSocketCallback())) {
  agent_host_->AttachClient(this);
  ParseLog(log);
}

DevToolsProtocolTestBindings::~DevToolsProtocolTestBindings() {
  if (agent_host_) {
    agent_host_->DetachClient(this);
    agent_host_ = nullptr;
  }
}

// static
GURL DevToolsProtocolTestBindings::MapTestURLIfNeeded(const GURL& test_url,
                                                      bool* is_protocol_test) {
  *is_protocol_test = false;
  std::string spec = test_url.spec();
  std::string dir = "/inspector-protocol/";
  size_t pos = spec.find(dir);
  if (pos == std::string::npos)
    return test_url;
  if (spec.rfind(".js") != spec.length() - 3)
    return test_url;
  spec = spec.substr(0, pos + dir.length()) +
         "resources/inspector-protocol-test.html?test=" + spec;
  if (base::CommandLine::ForCurrentProcess()->HasSwitch(
          switches::kDebugDevTools))
    spec += "&debug=true";
  *is_protocol_test = true;
  return GURL(spec);
}

void DevToolsProtocolTestBindings::ParseLog(std::string_view log) {
  if (log.empty()) {
    return;
  }
  std::vector<std::string> lines = base::SplitStringUsingSubstr(
      log, "\n", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
  for (const std::string& line : lines) {
    std::optional<base::Value::Dict> item =
        base::JSONReader::ReadDict(line, base::JSON_PARSE_CHROMIUM_EXTENSIONS);
    CHECK(!item->empty());
    log_.push_back(std::move(item.value()));
  }
  log_enabled_ = true;
}

void DevToolsProtocolTestBindings::ReadyToCommitNavigation(
    NavigationHandle* navigation_handle) {
#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS) && !BUILDFLAG(IS_FUCHSIA)
  content::RenderFrameHost* frame = navigation_handle->GetRenderFrameHost();
  if (frame->GetParent())
    return;
  frontend_host_ = DevToolsFrontendHost::Create(
      frame,
      base::BindRepeating(&DevToolsProtocolTestBindings::HandleMessageFromTest,
                          base::Unretained(this)));
#endif
}

void DevToolsProtocolTestBindings::WebContentsDestroyed() {
  if (agent_host_) {
    agent_host_->DetachClient(this);
    agent_host_ = nullptr;
  }
}

void DevToolsProtocolTestBindings::HandleMessagesFromLog(
    std::string_view protocol_message_string) {
  std::optional<base::Value::Dict> parsed = base::JSONReader::ReadDict(
      protocol_message_string, base::JSON_PARSE_CHROMIUM_EXTENSIONS);
  if (!parsed) {
    return;
  }
  base::Value::Dict protocol_message = std::move(parsed.value());

  CHECK(log_pos_ < log_.size()) << "Test sent commands but the log is empty";
  const base::Value::Dict& top = log_[log_pos_];
  CHECK(protocol_message == top)
      << "Test sent a command that is not the next in the log \n"
      << protocol_message << "\n"
      << top;
  log_pos_++;
  while (log_pos_ < log_.size()) {
    const base::Value::Dict& item = log_[log_pos_];
    // Stop when the next command is encountered in the log.
    if (item.FindString("method") && item.FindInt("id")) {
      break;
    }
    log_pos_++;
    std::optional<std::string> str_message = base::WriteJson(item);
    CHECK(str_message) << "Could not convert log message to JSON";
    std::string param;
    base::EscapeJSONString(str_message.value(), true, &param);
    std::string javascript = "DevToolsAPI.dispatchMessage(" + param + ");";
    web_contents()->GetPrimaryMainFrame()->ExecuteJavaScriptForTests(
        base::UTF8ToUTF16(javascript), base::NullCallback(),
        ISOLATED_WORLD_ID_GLOBAL);
  }
}

void DevToolsProtocolTestBindings::HandleMessageFromTest(
    base::Value::Dict message) {
  const std::string* method = message.FindString("method");
  if (!method)
    return;

  const base::Value::List* params = message.FindList("params");
  if (*method == "dispatchProtocolMessage" && params && params->size() == 1) {
    const std::string* protocol_message = (*params)[0].GetIfString();
    if (!protocol_message)
      return;

    if (log_enabled_) {
      HandleMessagesFromLog(*protocol_message);
      return;
    }

    if (agent_host_) {
      WebTestControlHost::Get()->PrintMessageToStderr(
          "Protocol message: " + *protocol_message + "\n");
      agent_host_->DispatchProtocolMessage(
          this, base::as_byte_span(*protocol_message));
    }
    return;
  }

  if (*method == "setAllowUnsafeOperations" && params && params->size() == 1) {
    allow_unsafe_operations_ = (*params)[0].GetIfBool().value_or(false);
  }
}

void DevToolsProtocolTestBindings::DispatchProtocolMessage(
    DevToolsAgentHost* agent_host,
    base::span<const uint8_t> message) {
  if (log_enabled_) {
    NOTREACHED() << "Unexpected messages dispatched by the browser";
  }
  std::string_view str_message(reinterpret_cast<const char*>(message.data()),
                                message.size());
  WebTestControlHost::Get()->PrintMessageToStderr(
      "Protocol message: " + std::string(str_message) + "\n");

  if (str_message.size() < kWebTestMaxMessageChunkSize) {
    std::string param;
    base::EscapeJSONString(str_message, true, &param);
    std::string code = "DevToolsAPI.dispatchMessage(" + param + ");";
    std::u16string javascript = base::UTF8ToUTF16(code);
    web_contents()->GetPrimaryMainFrame()->ExecuteJavaScriptForTests(
        javascript, base::NullCallback(), ISOLATED_WORLD_ID_GLOBAL);
    return;
  }

  size_t total_size = str_message.length();
  for (size_t pos = 0; pos < str_message.length();
       pos += kWebTestMaxMessageChunkSize) {
    std::string param;
    base::EscapeJSONString(str_message.substr(pos, kWebTestMaxMessageChunkSize),
                           true, &param);
    std::string code = "DevToolsAPI.dispatchMessageChunk(" + param + "," +
                       base::NumberToString(pos ? 0 : total_size) + ");";
    std::u16string javascript = base::UTF8ToUTF16(code);
    web_contents()->GetPrimaryMainFrame()->ExecuteJavaScriptForTests(
        javascript, base::NullCallback(), ISOLATED_WORLD_ID_GLOBAL);
  }
}

void DevToolsProtocolTestBindings::AgentHostClosed(
    DevToolsAgentHost* agent_host) {
  agent_host_ = nullptr;
}

bool DevToolsProtocolTestBindings::AllowUnsafeOperations() {
  return allow_unsafe_operations_;
}

}  // namespace content