// 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 "chrome/renderer/extensions/api/extension_hooks_delegate.h"

#include <string_view>

#include "base/memory/raw_ptr.h"
#include "base/strings/stringprintf.h"
#include "base/test/bind.h"
#include "content/public/common/content_constants.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/api/messaging/messaging_endpoint.h"
#include "extensions/common/extension_builder.h"
#include "extensions/common/mojom/context_type.mojom.h"
#include "extensions/common/mojom/message_port.mojom-shared.h"
#include "extensions/renderer/api/messaging/message_target.h"
#include "extensions/renderer/api/messaging/messaging_util.h"
#include "extensions/renderer/api/messaging/native_renderer_messaging_service.h"
#include "extensions/renderer/api/messaging/send_message_tester.h"
#include "extensions/renderer/api/runtime_hooks_delegate.h"
#include "extensions/renderer/bindings/api_binding_test_util.h"
#include "extensions/renderer/native_extension_bindings_system.h"
#include "extensions/renderer/native_extension_bindings_system_test_base.h"
#include "extensions/renderer/script_context.h"
#include "extensions/renderer/script_context_set.h"

static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));

namespace extensions {

class ExtensionHooksDelegateTest
    : public NativeExtensionBindingsSystemUnittest {
 public:
  ExtensionHooksDelegateTest() = default;

  ExtensionHooksDelegateTest(const ExtensionHooksDelegateTest&) = delete;
  ExtensionHooksDelegateTest& operator=(const ExtensionHooksDelegateTest&) =
      delete;

  ~ExtensionHooksDelegateTest() override = default;

  // NativeExtensionBindingsSystemUnittest:
  void SetUp() override {
    NativeExtensionBindingsSystemUnittest::SetUp();
    messaging_service_ =
        std::make_unique<NativeRendererMessagingService>(bindings_system());

    bindings_system()->api_system()->RegisterHooksDelegate(
        "extension",
        std::make_unique<ExtensionHooksDelegate>(messaging_service_.get()));
    bindings_system()->api_system()->RegisterHooksDelegate(
        "runtime",
        std::make_unique<RuntimeHooksDelegate>(messaging_service_.get()));

    scoped_refptr<const Extension> mutable_extension = BuildExtension();
    RegisterExtension(mutable_extension);
    extension_ = mutable_extension;

    v8::HandleScope handle_scope(isolate());
    v8::Local<v8::Context> context = MainContext();

    script_context_ =
        CreateScriptContext(context, mutable_extension.get(),
                            mojom::ContextType::kPrivilegedExtension);
    script_context_->set_url(extension_->url());
    bindings_system()->UpdateBindingsForContext(script_context_);
  }
  void TearDown() override {
    messaging_service_->InvalidatePorts(script_context_);
    script_context_ = nullptr;
    extension_ = nullptr;
    messaging_service_.reset();
    NativeExtensionBindingsSystemUnittest::TearDown();
  }
  bool UseStrictIPCMessageSender() override { return true; }

  virtual scoped_refptr<const Extension> BuildExtension() {
    // TODO(https://crbug.com/40804030): Update this to use MV3.
    // Some extension module methods only exist in MV2.
    return ExtensionBuilder("foo").SetManifestVersion(2).Build();
  }

  NativeRendererMessagingService* messaging_service() {
    return messaging_service_.get();
  }
  ScriptContext* script_context() { return script_context_; }
  const Extension* extension() { return extension_.get(); }

 private:
  std::unique_ptr<NativeRendererMessagingService> messaging_service_;

  raw_ptr<ScriptContext> script_context_ = nullptr;
  scoped_refptr<const Extension> extension_;
};

// Test chrome.extension messaging methods. Many of these are just aliased to
// chrome.runtime counterparts, but some others (like sendRequest) are
// implemented as hooks.
TEST_F(ExtensionHooksDelegateTest, MessagingSanityChecks) {
  v8::HandleScope handle_scope(isolate());

  MessageTarget self_target = MessageTarget::ForExtension(extension()->id());
  SendMessageTester tester(ipc_message_sender(), script_context(), 0,
                           "extension");

  tester.TestConnect("", "", self_target);

  constexpr char kStandardMessage[] = R"({"data":"hello"})";
  // We expect the port to remain OPEN for all these cases, as even when a
  // callback isn't supplied we return a promise which may be fulfilled with a
  // response if any of the associated event listeners choose to reply.
  tester.TestSendMessage("{data: 'hello'}", kStandardMessage, self_target,
                         SendMessageTester::OPEN);
  tester.TestSendMessage("{data: 'hello'}, function() {}", kStandardMessage,
                         self_target, SendMessageTester::OPEN);

  tester.TestSendRequest("{data: 'hello'}", kStandardMessage, self_target,
                         SendMessageTester::OPEN);
  tester.TestSendRequest("{data: 'hello'}, function() {}", kStandardMessage,
                         self_target, SendMessageTester::OPEN);

  // Ambiguous argument case ('hi' could be an id or a message); we massage it
  // into being the message because that's a required argument.
  tester.TestSendRequest("'hi', function() {}", "\"hi\"", self_target,
                         SendMessageTester::OPEN);
}

TEST_F(ExtensionHooksDelegateTest, SendRequestDisabled) {
  // Construct an extension for which sendRequest is disabled (unpacked
  // extension with an event page).
  scoped_refptr<const Extension> extension =
      ExtensionBuilder("foo")
          .SetManifestVersion(2)
          .SetBackgroundContext(ExtensionBuilder::BackgroundContext::EVENT_PAGE)
          .SetLocation(mojom::ManifestLocation::kUnpacked)
          .Build();
  RegisterExtension(extension);

  v8::HandleScope handle_scope(isolate());
  v8::Local<v8::Context> context = AddContext();
  ScriptContext* script_context = CreateScriptContext(
      context, extension.get(), mojom::ContextType::kPrivilegedExtension);
  script_context->set_url(extension->url());
  bindings_system()->UpdateBindingsForContext(script_context);
  ASSERT_TRUE(messaging_util::IsSendRequestDisabled(script_context));

  enum AccessBehavior {
    THROWS,
    DOESNT_THROW,
  };

  auto check_access = [context](const char* object, AccessBehavior behavior) {
    SCOPED_TRACE(object);
    constexpr char kExpectedError[] =
        "Uncaught Error: extension.sendRequest, extension.onRequest, and "
        "extension.onRequestExternal are deprecated. Please use "
        "runtime.sendMessage, runtime.onMessage, and runtime.onMessageExternal "
        "instead.";
    v8::Local<v8::Function> function = FunctionFromString(
        context, base::StringPrintf("(function() {%s;})", object));
    if (behavior == THROWS)
      RunFunctionAndExpectError(function, context, 0, nullptr, kExpectedError);
    else
      RunFunction(function, context, 0, nullptr);
  };

  check_access("chrome.extension.sendRequest", THROWS);
  check_access("chrome.extension.onRequest", THROWS);
  check_access("chrome.extension.onRequestExternal", THROWS);
  check_access("chrome.extension.sendMessage", DOESNT_THROW);
  check_access("chrome.extension.onMessage", DOESNT_THROW);
  check_access("chrome.extension.onMessageExternal", DOESNT_THROW);
}

// Ensure that the extension.sendRequest() method doesn't close the channel if
// the listener does not reply and also does not return `true` (unlike the
// runtime.sendMessage() method, which will).
TEST_F(ExtensionHooksDelegateTest, SendRequestChannelLeftOpenToReplyAsync) {
  v8::HandleScope handle_scope(isolate());
  v8::Local<v8::Context> context = MainContext();

  constexpr char kRegisterListener[] =
      "(function() {\n"
      "  chrome.extension.onRequest.addListener(\n"
      "      function(message, sender, reply) {});\n"
      "})";
  v8::Local<v8::Function> add_listener =
      FunctionFromString(context, kRegisterListener);
  RunFunctionOnGlobal(add_listener, context, 0, nullptr);

  const std::string kChannel = "chrome.extension.sendRequest";
  base::UnguessableToken other_context_id = base::UnguessableToken::Create();
  const PortId port_id(other_context_id, 0, false,
                       mojom::SerializationFormat::kJson);

  NativeRendererMessagingService::TabConnectionInfo tab_connection_info;
  NativeRendererMessagingService::ExternalConnectionInfo
      external_connection_info;
  tab_connection_info.frame_id = 0;
  GURL source_url("http://example.com");
  // We'd normally also have a tab here (stored in `tab_connection_info.tab`),
  // but then we need a very large JSON object for it to comply with our
  // schema. Just pretend it's not there.
  external_connection_info.target_id = extension()->id();
  external_connection_info.source_endpoint =
      MessagingEndpoint::ForExtension(extension()->id());
  external_connection_info.source_url = source_url;
  external_connection_info.guest_process_id =
      content::kInvalidChildProcessUniqueId;
  external_connection_info.guest_render_frame_routing_id = 0;

  // Open a receiver for the message.
  mojo::PendingAssociatedRemote<mojom::MessagePortHost> port_host_remote;
  auto port_host_receiver =
      port_host_remote.InitWithNewEndpointAndPassReceiver();

  mojo::PendingAssociatedReceiver<mojom::MessagePort> port_receiver;
  auto port_remote = port_receiver.InitWithNewEndpointAndPassRemote();

  bool port_opened = false;
  messaging_service()->DispatchOnConnect(
      script_context_set(), port_id, mojom::ChannelType::kSendRequest, kChannel,
      tab_connection_info, external_connection_info, std::move(port_receiver),
      std::move(port_host_remote), nullptr,
      base::BindLambdaForTesting(
          [&port_opened](bool success) { port_opened = success; }));
  port_host_receiver.EnableUnassociatedUsage();
  port_remote.EnableUnassociatedUsage();
  EXPECT_TRUE(port_opened);
  EXPECT_TRUE(
      messaging_service()->HasPortForTesting(script_context(), port_id));

  // Post the message to the receiver. Since the receiver doesn't respond, the
  // channel should remain open.
  messaging_service()->DeliverMessage(
      script_context_set(), port_id,
      Message("\"message\"", /*user_gesture=*/false),
      /*restrict_to_render_frame=*/nullptr);
  ::testing::Mock::VerifyAndClearExpectations(ipc_message_sender());
  EXPECT_TRUE(
      messaging_service()->HasPortForTesting(script_context(), port_id));
}

// Tests that overriding the runtime equivalents of chrome.extension methods
// with accessors that throw does not cause a crash on access. Regression test
// for https://crbug.com/41450968.
TEST_F(ExtensionHooksDelegateTest, RuntimeAliasesCorrupted) {
  v8::HandleScope handle_scope(isolate());
  v8::Local<v8::Context> context = MainContext();

  // Set a trap on chrome.runtime.sendMessage.
  constexpr char kMutateChromeRuntime[] =
      R"((function() {
           Object.defineProperty(
               chrome.runtime, 'sendMessage',
               { get() { throw new Error('haha'); } });
         }))";
  RunFunctionOnGlobal(FunctionFromString(context, kMutateChromeRuntime),
                      context, 0, nullptr);

  // Touch chrome.extension.sendMessage, which is aliased to the runtime
  // version. Though an error is thrown, we shouldn't crash.
  constexpr char kTouchExtensionSendMessage[] =
      "(function() { chrome.extension.sendMessage; })";
  RunFunctionOnGlobal(FunctionFromString(context, kTouchExtensionSendMessage),
                      context, 0, nullptr);
}

// Ensure that HandleGetURL allows extension URLs and doesn't allow arbitrary
// non-extension URLs. Very similar to RuntimeHooksDeligateTest that tests a
// similar function.
TEST_F(ExtensionHooksDelegateTest, GetURL) {
  v8::HandleScope handle_scope(isolate());
  v8::Local<v8::Context> context = MainContext();

  auto get_url = [this, context](const char* args, const GURL& expected_url) {
    SCOPED_TRACE(base::StringPrintf("Args: `%s`", args));
    constexpr char kGetUrlTemplate[] =
        "(function() { return chrome.extension.getURL(%s); })";
    v8::Local<v8::Function> get_url =
        FunctionFromString(context, base::StringPrintf(kGetUrlTemplate, args));
    v8::Local<v8::Value> url = RunFunction(get_url, context, 0, nullptr);
    ASSERT_FALSE(url.IsEmpty());
    ASSERT_TRUE(url->IsString());
    EXPECT_EQ(expected_url.spec(), gin::V8ToString(isolate(), url));
  };

  get_url("''", extension()->url());
  get_url("'foo'", extension()->GetResourceURL("foo"));
  get_url("'/foo'", extension()->GetResourceURL("foo"));
  get_url("'https://www.google.com'",
          GURL(extension()->url().spec() + "https://www.google.com"));
}

class ExtensionHooksDelegateModernTest : public ExtensionHooksDelegateTest {
 public:
  ExtensionHooksDelegateModernTest() = default;
  ~ExtensionHooksDelegateModernTest() override = default;

  scoped_refptr<const Extension> BuildExtension() override {
    return ExtensionBuilder("foo").Build();
  }
};

TEST_F(ExtensionHooksDelegateModernTest, AliasesArentAvailable) {
  v8::HandleScope handle_scope(isolate());
  v8::Local<v8::Context> context = MainContext();

  auto script_to_value = [context](std::string_view source) {
    return V8ToString(V8ValueFromScriptSource(context, source), context);
  };

  EXPECT_EQ("undefined", script_to_value("chrome.extension.connect"));
  EXPECT_EQ("undefined", script_to_value("chrome.extension.connectNative"));
  EXPECT_EQ("undefined", script_to_value("chrome.extension.getURL"));
  EXPECT_EQ("undefined", script_to_value("chrome.extension.onConnect"));
  EXPECT_EQ("undefined", script_to_value("chrome.extension.onConnectExternal"));
  EXPECT_EQ("undefined", script_to_value("chrome.extension.onMessage"));
  EXPECT_EQ("undefined", script_to_value("chrome.extension.onMessageExternal"));
  EXPECT_EQ("undefined", script_to_value("chrome.extension.onRequest"));
  EXPECT_EQ("undefined", script_to_value("chrome.extension.onRequestExternal"));
  EXPECT_EQ("undefined", script_to_value("chrome.extension.sendNativeMessage"));
  EXPECT_EQ("undefined", script_to_value("chrome.extension.sendMessage"));
  EXPECT_EQ("undefined", script_to_value("chrome.extension.sendRequest"));
}

}  // namespace extensions
