blob: 848adabff0fb26cf8b740606985b5a159d5ae1a9 [file] [log] [blame]
// Copyright 2020 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 <string>
#include "base/json/json_writer.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "base/values.h"
#include "chrome/browser/extensions/extension_browsertest.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/test/base/ui_test_utils.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/test_navigation_observer.h"
#include "extensions/browser/extension_api_frame_id_map.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/script_executor.h"
#include "extensions/common/extension_builder.h"
#include "extensions/common/user_script.h"
#include "net/dns/mock_host_resolver.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
namespace extensions {
namespace {
// A helper object to wait for and collect the results from a script execution.
class ScriptExecutorHelper {
public:
ScriptExecutorHelper() = default;
ScriptExecutorHelper(const ScriptExecutorHelper&) = delete;
ScriptExecutorHelper& operator=(const ScriptExecutorHelper&) = delete;
~ScriptExecutorHelper() = default;
void Wait() { run_loop_.Run(); }
ScriptExecutor::ScriptFinishedCallback GetCallback() {
// The Unretained() is safe because this object is always supposed to
// outlive the script execution.
return base::BindOnce(&ScriptExecutorHelper::OnScriptFinished,
base::Unretained(this));
}
const std::string& error() const { return error_; }
const GURL& url() const { return url_; }
const base::Value& result() const { return result_; }
private:
void OnScriptFinished(const std::string& error,
const GURL& url,
const base::ListValue& result) {
error_ = error;
url_ = url;
result_ = result.Clone();
run_loop_.Quit();
}
// Note: we include an initial value for the error so that asserting it's
// empty is meaningful.
std::string error_{"<initial error>"};
GURL url_;
base::Value result_;
base::RunLoop run_loop_;
};
} // namespace
class ScriptExecutorBrowserTest : public ExtensionBrowserTest {
public:
ScriptExecutorBrowserTest() = default;
ScriptExecutorBrowserTest(const ScriptExecutorBrowserTest&) = delete;
ScriptExecutorBrowserTest& operator=(const ScriptExecutorBrowserTest&) =
delete;
~ScriptExecutorBrowserTest() override = default;
const Extension* LoadExtensionWithHostPermission(
const std::string& host_permission) {
scoped_refptr<const Extension> extension =
ExtensionBuilder("extension").AddPermission(host_permission).Build();
extension_service()->AddExtension(extension.get());
EXPECT_TRUE(
extension_registry()->enabled_extensions().GetByID(extension->id()));
return extension.get();
}
void SetUpOnMainThread() override {
ExtensionBrowserTest::SetUpOnMainThread();
host_resolver()->AddRule("*", "127.0.0.1");
ASSERT_TRUE(embedded_test_server()->Start());
}
// Returns the frame with the given `name` from `web_contents`.
content::RenderFrameHost* GetFrameByName(content::WebContents* web_contents,
const std::string& name) {
return content::FrameMatchingPredicate(
web_contents, base::BindRepeating(&content::FrameMatchesName, name));
}
};
IN_PROC_BROWSER_TEST_F(ScriptExecutorBrowserTest, MainFrameExecution) {
const Extension* extension =
LoadExtensionWithHostPermission("http://example.com/*");
GURL example_com =
embedded_test_server()->GetURL("example.com", "/simple.html");
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
ASSERT_TRUE(web_contents);
{
content::TestNavigationObserver nav_observer(web_contents);
ui_test_utils::NavigateToURL(browser(), example_com);
nav_observer.Wait();
EXPECT_TRUE(nav_observer.last_navigation_succeeded());
}
EXPECT_EQ("OK", base::UTF16ToUTF8(web_contents->GetTitle()));
ScriptExecutor script_executor(web_contents);
constexpr char kCode[] =
R"(let oldTitle = document.title;
document.title = 'New Title';
oldTitle;
)";
ScriptExecutorHelper helper;
script_executor.ExecuteScript(
HostID(HostID::EXTENSIONS, extension->id()), UserScript::ADD_JAVASCRIPT,
kCode, ScriptExecutor::SPECIFIED_FRAMES,
{ExtensionApiFrameIdMap::kTopFrameId},
ScriptExecutor::DONT_MATCH_ABOUT_BLANK, UserScript::DOCUMENT_IDLE,
ScriptExecutor::DEFAULT_PROCESS, GURL() /* webview_src */,
GURL() /* script_url */, false /* user_gesture */,
base::nullopt /* css_origin */, ScriptExecutor::JSON_SERIALIZED_RESULT,
helper.GetCallback());
helper.Wait();
EXPECT_EQ("New Title", base::UTF16ToUTF8(web_contents->GetTitle()));
EXPECT_EQ(web_contents->GetLastCommittedURL(), helper.url());
EXPECT_EQ("", helper.error());
base::Value expected(base::Value::Type::LIST);
expected.Append("OK");
EXPECT_EQ(expected, helper.result());
}
// Tests script execution into a specified set of frames.
IN_PROC_BROWSER_TEST_F(ScriptExecutorBrowserTest, SpecifiedFrames) {
const Extension* extension =
LoadExtensionWithHostPermission("http://example.com/*");
GURL example_com = embedded_test_server()->GetURL(
"example.com", "/extensions/iframes/main.html");
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
ASSERT_TRUE(web_contents);
{
content::TestNavigationObserver nav_observer(web_contents);
ui_test_utils::NavigateToURL(browser(), example_com);
nav_observer.Wait();
EXPECT_TRUE(nav_observer.last_navigation_succeeded());
}
// Note: The frame hierarchy for main.html looks like:
// main
// frame1
// frame2
// frame2_child
// frame3
content::RenderFrameHost* frame1 = GetFrameByName(web_contents, "frame1");
ASSERT_TRUE(frame1);
const int frame1_id = ExtensionApiFrameIdMap::GetFrameId(frame1);
content::RenderFrameHost* frame2 = GetFrameByName(web_contents, "frame2");
ASSERT_TRUE(frame2);
const int frame2_id = ExtensionApiFrameIdMap::GetFrameId(frame2);
content::RenderFrameHost* frame3 = GetFrameByName(web_contents, "frame3");
ASSERT_TRUE(frame3);
content::RenderFrameHost* frame2_child =
GetFrameByName(web_contents, "frame2_child");
ASSERT_TRUE(frame2_child);
ScriptExecutor script_executor(web_contents);
// Note: Since other tests verify the code's effects, here we just rely on the
// execution result as an indication that it ran.
constexpr char kCode[] = "document.title;";
{
// Execute in frames 1 and 2. These are the only frames for which we should
// get a result.
ScriptExecutorHelper helper;
script_executor.ExecuteScript(
HostID(HostID::EXTENSIONS, extension->id()), UserScript::ADD_JAVASCRIPT,
kCode, ScriptExecutor::SPECIFIED_FRAMES, {frame1_id, frame2_id},
ScriptExecutor::DONT_MATCH_ABOUT_BLANK, UserScript::DOCUMENT_IDLE,
ScriptExecutor::DEFAULT_PROCESS, GURL() /* webview_src */,
GURL() /* script_url */, false /* user_gesture */,
base::nullopt /* css_origin */, ScriptExecutor::JSON_SERIALIZED_RESULT,
helper.GetCallback());
helper.Wait();
EXPECT_EQ("", helper.error());
base::Value frame1_result("Frame 1");
base::Value frame2_result("Frame 2");
EXPECT_THAT(
helper.result().GetList(),
testing::UnorderedElementsAre(::testing::Eq(std::cref(frame1_result)),
::testing::Eq(std::cref(frame2_result))));
}
{
// Repeat the execution in frames 1 and 2, but include subframes. This
// should result in frame2_child being added to the results.
ScriptExecutorHelper helper;
script_executor.ExecuteScript(
HostID(HostID::EXTENSIONS, extension->id()), UserScript::ADD_JAVASCRIPT,
kCode, ScriptExecutor::INCLUDE_SUB_FRAMES, {frame1_id, frame2_id},
ScriptExecutor::DONT_MATCH_ABOUT_BLANK, UserScript::DOCUMENT_IDLE,
ScriptExecutor::DEFAULT_PROCESS, GURL() /* webview_src */,
GURL() /* script_url */, false /* user_gesture */,
base::nullopt /* css_origin */, ScriptExecutor::JSON_SERIALIZED_RESULT,
helper.GetCallback());
helper.Wait();
EXPECT_EQ("", helper.error());
base::Value frame1_result("Frame 1");
base::Value frame2_result("Frame 2");
base::Value frame2_child_result("Frame 2 Child");
EXPECT_THAT(helper.result().GetList(),
testing::UnorderedElementsAre(
::testing::Eq(std::cref(frame1_result)),
::testing::Eq(std::cref(frame2_result)),
::testing::Eq(std::cref(frame2_child_result))));
}
// Note: we don't use ExtensionApiFrameIdMap::kInvalidFrameId because we want
// to target a "potentially valid" frame (emulating a frame that used to
// exist, but no longer does).
constexpr int kNonExistentFrameId = 99999;
EXPECT_EQ(nullptr, ExtensionApiFrameIdMap::GetRenderFrameHostById(
web_contents, kNonExistentFrameId));
{
// Try injecting into multiple frames when one of the specified frames
// doesn't exist.
ScriptExecutorHelper helper;
script_executor.ExecuteScript(
HostID(HostID::EXTENSIONS, extension->id()), UserScript::ADD_JAVASCRIPT,
kCode, ScriptExecutor::SPECIFIED_FRAMES,
{frame1_id, frame2_id, kNonExistentFrameId},
ScriptExecutor::DONT_MATCH_ABOUT_BLANK, UserScript::DOCUMENT_IDLE,
ScriptExecutor::DEFAULT_PROCESS, GURL() /* webview_src */,
GURL() /* script_url */, false /* user_gesture */,
base::nullopt /* css_origin */, ScriptExecutor::JSON_SERIALIZED_RESULT,
helper.GetCallback());
helper.Wait();
// When specifying multiple frames, if one doesn't exist, the rest of the
// injections should succeed and there should be no error.
EXPECT_EQ("", helper.error());
base::Value frame1_result("Frame 1");
base::Value frame2_result("Frame 2");
EXPECT_THAT(
helper.result().GetList(),
testing::UnorderedElementsAre(::testing::Eq(std::cref(frame1_result)),
::testing::Eq(std::cref(frame2_result))));
}
{
// Try injecting into a single non-existent frame.
ScriptExecutorHelper helper;
script_executor.ExecuteScript(
HostID(HostID::EXTENSIONS, extension->id()), UserScript::ADD_JAVASCRIPT,
kCode, ScriptExecutor::SPECIFIED_FRAMES, {kNonExistentFrameId},
ScriptExecutor::DONT_MATCH_ABOUT_BLANK, UserScript::DOCUMENT_IDLE,
ScriptExecutor::DEFAULT_PROCESS, GURL() /* webview_src */,
GURL() /* script_url */, false /* user_gesture */,
base::nullopt /* css_origin */, ScriptExecutor::JSON_SERIALIZED_RESULT,
helper.GetCallback());
helper.Wait();
// If only a single frame was specified and it doesn't exist, the call
// should return an error.
EXPECT_EQ("The frame was removed.", helper.error());
EXPECT_TRUE(helper.result().GetList().empty());
}
}
} // namespace extensions