blob: 04a4d0f2171229a5d3397c8e16c04629789276e3 [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// 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/mojom/css_origin.mojom-shared.h"
#include "extensions/common/mojom/host_id.mojom.h"
#include "extensions/common/mojom/run_location.mojom-shared.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::vector<ScriptExecutor::FrameResult>& results() const {
return results_;
}
private:
void OnScriptFinished(
std::vector<ScriptExecutor::FrameResult> frame_results) {
results_ = std::move(frame_results);
run_loop_.Quit();
}
std::vector<ScriptExecutor::FrameResult> results_;
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->GetPrimaryPage(),
base::BindRepeating(&content::FrameMatchesName, name));
}
};
IN_PROC_BROWSER_TEST_F(ScriptExecutorBrowserTest, MainWorldExecution) {
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);
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), example_com));
nav_observer.Wait();
EXPECT_TRUE(nav_observer.last_navigation_succeeded());
}
content::RenderFrameHost* main_frame = web_contents->GetPrimaryMainFrame();
constexpr char kSetFlagScript[] = "window.mainWorldFlag = 'executionFlag';";
// NOTE: We use ExecuteScript() (and not EvalJs or ExecJs) because we
// explicitly *need* this to happen in the main world for the test.
EXPECT_TRUE(content::ExecuteScript(main_frame, kSetFlagScript));
ScriptExecutor script_executor(web_contents);
ScriptExecutorHelper helper;
std::vector<mojom::JSSourcePtr> sources;
sources.push_back(mojom::JSSource::New("window.mainWorldFlag", GURL()));
script_executor.ExecuteScript(
mojom::HostID(mojom::HostID::HostType::kExtensions, extension->id()),
mojom::CodeInjection::NewJs(mojom::JSInjection::New(
std::move(sources), mojom::ExecutionWorld::kMain,
blink::mojom::WantResultOption::kWantResult,
blink::mojom::UserActivationOption::kDoNotActivate,
blink::mojom::PromiseResultOption::kAwait)),
ScriptExecutor::SPECIFIED_FRAMES, {ExtensionApiFrameIdMap::kTopFrameId},
ScriptExecutor::DONT_MATCH_ABOUT_BLANK, mojom::RunLocation::kDocumentIdle,
ScriptExecutor::DEFAULT_PROCESS, GURL() /* webview_src */,
helper.GetCallback());
helper.Wait();
ASSERT_EQ(1u, helper.results().size());
EXPECT_EQ(web_contents->GetLastCommittedURL(), helper.results()[0].url);
EXPECT_EQ(base::Value("executionFlag"), helper.results()[0].value);
EXPECT_EQ(0, helper.results()[0].frame_id);
EXPECT_EQ("", helper.results()[0].error);
}
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);
ASSERT_TRUE(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;
std::vector<mojom::JSSourcePtr> sources;
sources.push_back(mojom::JSSource::New(kCode, GURL()));
script_executor.ExecuteScript(
mojom::HostID(mojom::HostID::HostType::kExtensions, extension->id()),
mojom::CodeInjection::NewJs(mojom::JSInjection::New(
std::move(sources), mojom::ExecutionWorld::kIsolated,
blink::mojom::WantResultOption::kWantResult,
blink::mojom::UserActivationOption::kDoNotActivate,
blink::mojom::PromiseResultOption::kAwait)),
ScriptExecutor::SPECIFIED_FRAMES, {ExtensionApiFrameIdMap::kTopFrameId},
ScriptExecutor::DONT_MATCH_ABOUT_BLANK, mojom::RunLocation::kDocumentIdle,
ScriptExecutor::DEFAULT_PROCESS, GURL() /* webview_src */,
helper.GetCallback());
helper.Wait();
EXPECT_EQ("New Title", base::UTF16ToUTF8(web_contents->GetTitle()));
ASSERT_EQ(1u, helper.results().size());
EXPECT_EQ(web_contents->GetLastCommittedURL(), helper.results()[0].url);
EXPECT_EQ(base::Value("OK"), helper.results()[0].value);
EXPECT_EQ(0, helper.results()[0].frame_id);
EXPECT_EQ("", helper.results()[0].error);
}
// Tests injecting multiple JS sources into a frame.
IN_PROC_BROWSER_TEST_F(ScriptExecutorBrowserTest, MultipleSourceExecution) {
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);
ASSERT_TRUE(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()));
// Inject two pieces of code. Note that the second references a variable set
// by the first, which thus also exercises injection order (in addition to
// that they both run).
ScriptExecutor script_executor(web_contents);
constexpr char kCode1[] =
R"(window.newTitle = 'New Title';
'First Result';)";
constexpr char kCode2[] =
R"(document.title = window.newTitle;
'Second Result';)";
ScriptExecutorHelper helper;
std::vector<mojom::JSSourcePtr> sources;
sources.push_back(mojom::JSSource::New(kCode1, GURL()));
sources.push_back(mojom::JSSource::New(kCode2, GURL()));
script_executor.ExecuteScript(
mojom::HostID(mojom::HostID::HostType::kExtensions, extension->id()),
mojom::CodeInjection::NewJs(mojom::JSInjection::New(
std::move(sources), mojom::ExecutionWorld::kIsolated,
blink::mojom::WantResultOption::kWantResult,
blink::mojom::UserActivationOption::kDoNotActivate,
blink::mojom::PromiseResultOption::kAwait)),
ScriptExecutor::SPECIFIED_FRAMES, {ExtensionApiFrameIdMap::kTopFrameId},
ScriptExecutor::DONT_MATCH_ABOUT_BLANK, mojom::RunLocation::kDocumentIdle,
ScriptExecutor::DEFAULT_PROCESS, GURL() /* webview_src */,
helper.GetCallback());
helper.Wait();
EXPECT_EQ("New Title", base::UTF16ToUTF8(web_contents->GetTitle()));
ASSERT_EQ(1u, helper.results().size());
EXPECT_EQ(web_contents->GetLastCommittedURL(), helper.results()[0].url);
EXPECT_EQ(base::Value("Second Result"), helper.results()[0].value);
EXPECT_EQ(0, helper.results()[0].frame_id);
EXPECT_EQ("", helper.results()[0].error);
}
// Tests that scripts that evaluate to promises can be properly waited upon.
IN_PROC_BROWSER_TEST_F(ScriptExecutorBrowserTest, PromisesResolve) {
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);
ASSERT_TRUE(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);
{
// Inject two pieces of code. They each evaluate to a promise. The second,
// `kCode2`, evaluates to a promise that resolves immediately, and then
// asynchronously resovles the promise from the first, `kCode1`, which
// changes the title of the page.
// This guarantees that the renderer code properly waits for *all* results
// to resolve, and not simply the last one.
constexpr char kCode1[] =
R"((new Promise((resolve) => {
window.resolveFirstPromise = resolve;
}).then(() => {
document.title = 'New Title';
}));)";
constexpr char kCode2[] =
R"((new Promise((resolve) => {
resolve('Second Promise');
setTimeout(window.resolveFirstPromise, 0);
}));)";
ScriptExecutorHelper helper;
std::vector<mojom::JSSourcePtr> sources;
sources.push_back(mojom::JSSource::New(kCode1, GURL()));
sources.push_back(mojom::JSSource::New(kCode2, GURL()));
script_executor.ExecuteScript(
mojom::HostID(mojom::HostID::HostType::kExtensions, extension->id()),
mojom::CodeInjection::NewJs(mojom::JSInjection::New(
std::move(sources), mojom::ExecutionWorld::kIsolated,
blink::mojom::WantResultOption::kWantResult,
blink::mojom::UserActivationOption::kDoNotActivate,
blink::mojom::PromiseResultOption::kAwait)),
ScriptExecutor::SPECIFIED_FRAMES, {ExtensionApiFrameIdMap::kTopFrameId},
ScriptExecutor::DONT_MATCH_ABOUT_BLANK,
mojom::RunLocation::kDocumentIdle, ScriptExecutor::DEFAULT_PROCESS,
GURL() /* webview_src */, helper.GetCallback());
helper.Wait();
EXPECT_EQ("New Title", base::UTF16ToUTF8(web_contents->GetTitle()));
ASSERT_EQ(1u, helper.results().size());
EXPECT_EQ(web_contents->GetLastCommittedURL(), helper.results()[0].url);
EXPECT_EQ(base::Value("Second Promise"), helper.results()[0].value);
EXPECT_EQ(0, helper.results()[0].frame_id);
EXPECT_EQ("", helper.results()[0].error);
}
{
// Next, inject code that evaluates to a promise, but don't include the
// "wait_for_promise" flag. The returned result should be the promise
// itself, which then serializes to an empty object (`{}`).
constexpr char kCode[] = R"((new Promise((r) => { r('hello'); }));)";
ScriptExecutorHelper helper;
std::vector<mojom::JSSourcePtr> sources;
sources.push_back(mojom::JSSource::New(kCode, GURL()));
script_executor.ExecuteScript(
mojom::HostID(mojom::HostID::HostType::kExtensions, extension->id()),
mojom::CodeInjection::NewJs(mojom::JSInjection::New(
std::move(sources), mojom::ExecutionWorld::kIsolated,
blink::mojom::WantResultOption::kWantResult,
blink::mojom::UserActivationOption::kDoNotActivate,
blink::mojom::PromiseResultOption::kDoNotWait)),
ScriptExecutor::SPECIFIED_FRAMES, {ExtensionApiFrameIdMap::kTopFrameId},
ScriptExecutor::DONT_MATCH_ABOUT_BLANK,
mojom::RunLocation::kDocumentIdle, ScriptExecutor::DEFAULT_PROCESS,
GURL() /* webview_src */, helper.GetCallback());
helper.Wait();
ASSERT_EQ(1u, helper.results().size());
EXPECT_EQ(web_contents->GetLastCommittedURL(), helper.results()[0].url);
EXPECT_EQ(base::Value(base::Value::Type::DICTIONARY),
helper.results()[0].value);
EXPECT_EQ(0, helper.results()[0].frame_id);
EXPECT_EQ("", helper.results()[0].error);
}
}
// 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);
ASSERT_TRUE(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);
const GURL frame1_url = frame1->GetLastCommittedURL();
content::RenderFrameHost* frame2 = GetFrameByName(web_contents, "frame2");
ASSERT_TRUE(frame2);
const int frame2_id = ExtensionApiFrameIdMap::GetFrameId(frame2);
const GURL frame2_url = frame2->GetLastCommittedURL();
content::RenderFrameHost* frame3 = GetFrameByName(web_contents, "frame3");
ASSERT_TRUE(frame3);
content::RenderFrameHost* frame2_child =
GetFrameByName(web_contents, "frame2_child");
ASSERT_TRUE(frame2_child);
const int frame2_child_id = ExtensionApiFrameIdMap::GetFrameId(frame2_child);
const GURL frame2_child_url = frame2_child->GetLastCommittedURL();
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;";
const base::Value frame1_result("Frame 1");
const base::Value frame2_result("Frame 2");
const base::Value frame2_child_result("Frame 2 Child");
auto get_result_matcher = [](const base::Value& value, int frame_id,
const GURL& url, const std::string& error = "") {
return ::testing::AllOf(
::testing::Field(&ScriptExecutor::FrameResult::value,
::testing::Eq(std::cref(value))),
::testing::Field(&ScriptExecutor::FrameResult::frame_id, frame_id),
::testing::Field(&ScriptExecutor::FrameResult::url, url),
::testing::Field(&ScriptExecutor::FrameResult::error, error));
};
{
// Execute in frames 1 and 2. These are the only frames for which we should
// get a result.
ScriptExecutorHelper helper;
std::vector<mojom::JSSourcePtr> sources;
sources.push_back(mojom::JSSource::New(kCode, GURL()));
script_executor.ExecuteScript(
mojom::HostID(mojom::HostID::HostType::kExtensions, extension->id()),
mojom::CodeInjection::NewJs(mojom::JSInjection::New(
std::move(sources), mojom::ExecutionWorld::kIsolated,
blink::mojom::WantResultOption::kWantResult,
blink::mojom::UserActivationOption::kDoNotActivate,
blink::mojom::PromiseResultOption::kAwait)),
ScriptExecutor::SPECIFIED_FRAMES, {frame1_id, frame2_id},
ScriptExecutor::DONT_MATCH_ABOUT_BLANK,
mojom::RunLocation::kDocumentIdle, ScriptExecutor::DEFAULT_PROCESS,
GURL() /* webview_src */, helper.GetCallback());
helper.Wait();
EXPECT_THAT(helper.results(),
testing::UnorderedElementsAre(
get_result_matcher(frame1_result, frame1_id, frame1_url),
get_result_matcher(frame2_result, frame2_id, frame2_url)));
}
{
// Repeat the execution in frames 1 and 2, but include subframes. This
// should result in frame2_child being added to the results.
ScriptExecutorHelper helper;
std::vector<mojom::JSSourcePtr> sources;
sources.push_back(mojom::JSSource::New(kCode, GURL()));
script_executor.ExecuteScript(
mojom::HostID(mojom::HostID::HostType::kExtensions, extension->id()),
mojom::CodeInjection::NewJs(mojom::JSInjection::New(
std::move(sources), mojom::ExecutionWorld::kIsolated,
blink::mojom::WantResultOption::kWantResult,
blink::mojom::UserActivationOption::kDoNotActivate,
blink::mojom::PromiseResultOption::kAwait)),
ScriptExecutor::INCLUDE_SUB_FRAMES, {frame1_id, frame2_id},
ScriptExecutor::DONT_MATCH_ABOUT_BLANK,
mojom::RunLocation::kDocumentIdle, ScriptExecutor::DEFAULT_PROCESS,
GURL() /* webview_src */, helper.GetCallback());
helper.Wait();
EXPECT_THAT(helper.results(),
testing::UnorderedElementsAre(
get_result_matcher(frame1_result, frame1_id, frame1_url),
get_result_matcher(frame2_result, frame2_id, frame2_url),
get_result_matcher(frame2_child_result, frame2_child_id,
frame2_child_url)));
}
// 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;
std::vector<mojom::JSSourcePtr> sources;
sources.push_back(mojom::JSSource::New(kCode, GURL()));
script_executor.ExecuteScript(
mojom::HostID(mojom::HostID::HostType::kExtensions, extension->id()),
mojom::CodeInjection::NewJs(mojom::JSInjection::New(
std::move(sources), mojom::ExecutionWorld::kIsolated,
blink::mojom::WantResultOption::kWantResult,
blink::mojom::UserActivationOption::kDoNotActivate,
blink::mojom::PromiseResultOption::kAwait)),
ScriptExecutor::SPECIFIED_FRAMES,
{frame1_id, frame2_id, kNonExistentFrameId},
ScriptExecutor::DONT_MATCH_ABOUT_BLANK,
mojom::RunLocation::kDocumentIdle, ScriptExecutor::DEFAULT_PROCESS,
GURL() /* webview_src */, helper.GetCallback());
helper.Wait();
EXPECT_THAT(helper.results(),
testing::UnorderedElementsAre(
get_result_matcher(frame1_result, frame1_id, frame1_url),
get_result_matcher(frame2_result, frame2_id, frame2_url),
get_result_matcher(base::Value(), kNonExistentFrameId,
GURL(), "No frame with ID: 99999")));
}
{
// Try injecting into a single non-existent frame.
ScriptExecutorHelper helper;
std::vector<mojom::JSSourcePtr> sources;
sources.push_back(mojom::JSSource::New(kCode, GURL()));
script_executor.ExecuteScript(
mojom::HostID(mojom::HostID::HostType::kExtensions, extension->id()),
mojom::CodeInjection::NewJs(mojom::JSInjection::New(
std::move(sources), mojom::ExecutionWorld::kIsolated,
blink::mojom::WantResultOption::kWantResult,
blink::mojom::UserActivationOption::kDoNotActivate,
blink::mojom::PromiseResultOption::kAwait)),
ScriptExecutor::SPECIFIED_FRAMES, {kNonExistentFrameId},
ScriptExecutor::DONT_MATCH_ABOUT_BLANK,
mojom::RunLocation::kDocumentIdle, ScriptExecutor::DEFAULT_PROCESS,
GURL() /* webview_src */, helper.GetCallback());
helper.Wait();
EXPECT_THAT(helper.results(),
testing::UnorderedElementsAre(
get_result_matcher(base::Value(), kNonExistentFrameId,
GURL(), "No frame with ID: 99999")));
}
}
} // namespace extensions