blob: 281dc661aff361af40b2625df4a0f2c2da4d32ac [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 "content/browser/webui/web_ui_main_frame_observer.h"
#include <memory>
#include <string>
#include "base/gtest_prod_util.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/field_trial_params.h"
#include "base/task/single_thread_task_runner.h"
#include "build/build_config.h"
#include "components/crash/content/browser/error_reporting/javascript_error_report.h" // nogncheck
#include "components/crash/content/browser/error_reporting/js_error_report_processor.h" // nogncheck
#include "content/public/browser/site_instance.h"
#include "content/public/browser/web_ui_controller.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/navigation_simulator.h"
#include "content/public/test/test_browser_context.h"
#include "content/public/test/test_renderer_host.h"
#include "content/test/test_web_contents.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
namespace content {
namespace {
using testing::Eq;
using testing::Optional;
class FakeJsErrorReportProcessor : public JsErrorReportProcessor {
public:
explicit FakeJsErrorReportProcessor(
scoped_refptr<base::SingleThreadTaskRunner> task_runner)
: task_runner_(std::move(task_runner)) {}
// After calling this, expect all SendErrorReport's to use this
// BrowserContext.
void SetExpectedBrowserContext(BrowserContext* browser_context) {
browser_context_ = browser_context;
}
void SendErrorReport(JavaScriptErrorReport error_report,
base::OnceClosure completion_callback,
BrowserContext* browser_context) override {
CHECK_EQ(browser_context, browser_context_);
last_error_report_ = std::move(error_report);
++error_report_count_;
task_runner_->PostTask(FROM_HERE, std::move(completion_callback));
}
const JavaScriptErrorReport& last_error_report() const {
return last_error_report_;
}
int error_report_count() const { return error_report_count_; }
// Make public for testing.
using JsErrorReportProcessor::SetDefault;
protected:
~FakeJsErrorReportProcessor() override = default;
private:
scoped_refptr<base::SingleThreadTaskRunner> task_runner_;
JavaScriptErrorReport last_error_report_;
int error_report_count_ = 0;
raw_ptr<BrowserContext> browser_context_ = nullptr;
};
class MockWebUIController : public WebUIController {
public:
explicit MockWebUIController(WebUI* web_ui) : WebUIController(web_ui) {}
bool IsJavascriptErrorReportingEnabled() override {
return enable_javascript_error_reporting_;
}
void enable_javascript_error_reporting(
bool enable_javascript_error_reporting) {
enable_javascript_error_reporting_ = enable_javascript_error_reporting;
}
private:
bool enable_javascript_error_reporting_ = true;
WEB_UI_CONTROLLER_TYPE_DECL();
};
WEB_UI_CONTROLLER_TYPE_IMPL(MockWebUIController)
} // namespace
class WebUIMainFrameObserverTest : public RenderViewHostTestHarness {
public:
void SetUp() override {
RenderViewHostTestHarness::SetUp();
site_instance_ = SiteInstance::Create(browser_context());
SetContents(TestWebContents::Create(browser_context(), site_instance_));
// Since we just created the web_contents() pointer with
// TestWebContents::Create, the static_casts are safe.
web_ui_ = std::make_unique<WebUIImpl>(web_contents());
web_ui_->SetRenderFrameHost(
static_cast<TestWebContents*>(web_contents())->GetPrimaryMainFrame());
web_ui_->SetController(
std::make_unique<MockWebUIController>(web_ui_.get()));
process()->Init();
previous_processor_ = JsErrorReportProcessor::Get();
processor_ = base::MakeRefCounted<FakeJsErrorReportProcessor>(
task_environment()->GetMainThreadTaskRunner());
FakeJsErrorReportProcessor::SetDefault(processor_);
processor_->SetExpectedBrowserContext(browser_context());
}
void TearDown() override {
FakeJsErrorReportProcessor::SetDefault(previous_processor_);
// We have to destroy the site_instance_ before
// RenderViewHostTestHarness::TearDown() destroys the
// BrowserTaskEnvironment. We've gotten a lot of things that depend directly
// or indirectly on BrowserTaskEnvironment, so just destroy everything we
// can.
previous_processor_.reset();
processor_.reset();
web_ui_.reset();
site_instance_.reset();
RenderViewHostTestHarness::TearDown();
}
// Simulate navigating to the WebUI page. Basically so that
// WebUIMainFrameObserver::ReadyToCommitNavigation() gets called, since that
// initializes the error handling.
void NavigateToPage() {
NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(),
GURL(kPageURL8));
}
// Calls the observer's OnDidAddMessageToConsole with the given arguments.
// This is just here so that we don't need to FRIEND_TEST_ALL_PREFIXES for
// each and every test.
void CallOnDidAddMessageToConsole(
RenderFrameHost* source_frame,
blink::mojom::ConsoleMessageLevel log_level,
const std::u16string& message,
int32_t line_no,
const std::u16string& source_id,
const std::optional<std::u16string>& stack_trace) {
web_ui_->GetWebUIMainFrameObserverForTest()->OnDidAddMessageToConsole(
source_frame, log_level, message, line_no, source_id, stack_trace);
}
protected:
scoped_refptr<SiteInstance> site_instance_;
std::unique_ptr<WebUIImpl> web_ui_;
scoped_refptr<FakeJsErrorReportProcessor> processor_;
scoped_refptr<JsErrorReportProcessor> previous_processor_;
static constexpr char kMessage8[] = "An Error Is Me";
static constexpr char16_t kMessage16[] = u"An Error Is Me";
static constexpr char kSourceURL8[] = "chrome://here.is.error/bad.js";
static constexpr char16_t kSourceURL16[] = u"chrome://here.is.error/bad.js";
static constexpr char kPageURL8[] = "chrome://here.is.error/index.html";
static constexpr char kStackTrace8[] =
"at badFunction (chrome://page/my.js:20:30)\n"
"at poorCaller (chrome://page/my.js:50:10)\n";
static constexpr char16_t kStackTrace16[] =
u"at badFunction (chrome://page/my.js:20:30)\n"
u"at poorCaller (chrome://page/my.js:50:10)\n";
};
constexpr char WebUIMainFrameObserverTest::kMessage8[];
constexpr char16_t WebUIMainFrameObserverTest::kMessage16[];
constexpr char WebUIMainFrameObserverTest::kSourceURL8[];
constexpr char16_t WebUIMainFrameObserverTest::kSourceURL16[];
constexpr char WebUIMainFrameObserverTest::kPageURL8[];
constexpr char WebUIMainFrameObserverTest::kStackTrace8[];
constexpr char16_t WebUIMainFrameObserverTest::kStackTrace16[];
TEST_F(WebUIMainFrameObserverTest, ErrorReported) {
NavigateToPage();
CallOnDidAddMessageToConsole(web_ui_->GetRenderFrameHost(),
blink::mojom::ConsoleMessageLevel::kError,
kMessage16, 5, kSourceURL16, kStackTrace16);
task_environment()->RunUntilIdle();
EXPECT_EQ(processor_->error_report_count(), 1);
EXPECT_EQ(processor_->last_error_report().message, kMessage8);
EXPECT_EQ(processor_->last_error_report().url, kSourceURL8);
EXPECT_EQ(processor_->last_error_report().page_url, kPageURL8);
EXPECT_EQ(processor_->last_error_report().source_system,
JavaScriptErrorReport::SourceSystem::kWebUIObserver);
EXPECT_THAT(processor_->last_error_report().stack_trace,
Optional(Eq(kStackTrace8)));
// WebUI should use default product & version.
EXPECT_EQ(processor_->last_error_report().product, "");
EXPECT_EQ(processor_->last_error_report().version, "");
EXPECT_EQ(*processor_->last_error_report().line_number, 5);
EXPECT_FALSE(processor_->last_error_report().column_number);
EXPECT_FALSE(processor_->last_error_report().app_locale);
EXPECT_TRUE(processor_->last_error_report().send_to_production_servers);
}
TEST_F(WebUIMainFrameObserverTest, NoStackTrace) {
NavigateToPage();
CallOnDidAddMessageToConsole(web_ui_->GetRenderFrameHost(),
blink::mojom::ConsoleMessageLevel::kError,
kMessage16, 5, kSourceURL16, std::nullopt);
task_environment()->RunUntilIdle();
EXPECT_EQ(processor_->error_report_count(), 1);
EXPECT_EQ(processor_->last_error_report().stack_trace, std::nullopt);
}
TEST_F(WebUIMainFrameObserverTest, NonErrorsIgnored) {
NavigateToPage();
CallOnDidAddMessageToConsole(web_ui_->GetRenderFrameHost(),
blink::mojom::ConsoleMessageLevel::kWarning,
kMessage16, 5, kSourceURL16, kStackTrace16);
CallOnDidAddMessageToConsole(web_ui_->GetRenderFrameHost(),
blink::mojom::ConsoleMessageLevel::kInfo,
kMessage16, 5, kSourceURL16, kStackTrace16);
CallOnDidAddMessageToConsole(web_ui_->GetRenderFrameHost(),
blink::mojom::ConsoleMessageLevel::kVerbose,
kMessage16, 5, kSourceURL16, kStackTrace16);
task_environment()->RunUntilIdle();
EXPECT_EQ(processor_->error_report_count(), 0);
}
TEST_F(WebUIMainFrameObserverTest, NoProcessorDoesntCrash) {
NavigateToPage();
FakeJsErrorReportProcessor::SetDefault(nullptr);
CallOnDidAddMessageToConsole(web_ui_->GetRenderFrameHost(),
blink::mojom::ConsoleMessageLevel::kError,
kMessage16, 5, kSourceURL16, kStackTrace16);
task_environment()->RunUntilIdle();
}
TEST_F(WebUIMainFrameObserverTest, NotSentIfInvalidURL) {
NavigateToPage();
CallOnDidAddMessageToConsole(web_ui_->GetRenderFrameHost(),
blink::mojom::ConsoleMessageLevel::kError,
kMessage16, 5, u"invalid URL", kStackTrace16);
task_environment()->RunUntilIdle();
EXPECT_EQ(processor_->error_report_count(), 0);
}
TEST_F(WebUIMainFrameObserverTest, NotSentIfDisabledForPage) {
static_cast<MockWebUIController*>(web_ui_->GetController())
->enable_javascript_error_reporting(false);
NavigateToPage();
CallOnDidAddMessageToConsole(web_ui_->GetRenderFrameHost(),
blink::mojom::ConsoleMessageLevel::kError,
kMessage16, 5, kSourceURL16, kStackTrace16);
task_environment()->RunUntilIdle();
EXPECT_EQ(processor_->error_report_count(), 0);
}
TEST_F(WebUIMainFrameObserverTest, URLPathIsPreservedOtherPartsRemoved) {
NavigateToPage();
struct URLTest {
const char16_t* const input;
const char* const expected;
};
const URLTest kTests[] = {
// No path still has no path.
{u"chrome://version", "chrome://version/"},
{u"chrome://version/", "chrome://version/"},
// Path is kept.
{u"chrome://discards/graph", "chrome://discards/graph"},
{u"chrome://discards/graph/", "chrome://discards/graph/"},
// Longer paths are kept.
{u"chrome://discards/graph/a/b/c/d", "chrome://discards/graph/a/b/c/d"},
// Queries are removed, with or without a path.
{u"chrome://bookmarks/?q=chromium", "chrome://bookmarks/"},
{u"chrome://bookmarks/add?q=chromium", "chrome://bookmarks/add"},
{u"chrome://bookmarks/add/?q=chromium", "chrome://bookmarks/add/"},
// Fragments are removed, with or without a path.
{u"chrome://flags/#tab-groups", "chrome://flags/"},
{u"chrome://flags/available/#tab-groups", "chrome://flags/available/"},
// Queries & fragments are removed.
{u"chrome://bookmarks/add?q=chromium#code", "chrome://bookmarks/add"},
// User name and password are removed. (It's weird to have a user name or
// password on a chrome URL, but otherwise we get blocked by the
// no-non-chrome-URLs check)
{u"chrome://chronos:test0000@version/Home", "chrome://version/Home"},
};
for (const URLTest& test : kTests) {
int previous_count = processor_->error_report_count();
CallOnDidAddMessageToConsole(web_ui_->GetRenderFrameHost(),
blink::mojom::ConsoleMessageLevel::kError,
kMessage16, 5, test.input, kStackTrace16);
task_environment()->RunUntilIdle();
EXPECT_EQ(processor_->error_report_count(), previous_count + 1)
<< "for " << test.input;
EXPECT_EQ(processor_->last_error_report().url, test.expected)
<< "for " << test.input;
}
}
TEST_F(WebUIMainFrameObserverTest, PageURLAlsoRedacted) {
constexpr char kPageWithQueryAndFragment[] =
"chrome://bookmarks/add?q=chromium#code";
NavigationSimulator::NavigateAndCommitFromBrowser(
web_contents(), GURL(kPageWithQueryAndFragment));
CallOnDidAddMessageToConsole(web_ui_->GetRenderFrameHost(),
blink::mojom::ConsoleMessageLevel::kError,
kMessage16, 5, kSourceURL16, kStackTrace16);
task_environment()->RunUntilIdle();
EXPECT_EQ(processor_->error_report_count(), 1);
EXPECT_EQ(processor_->last_error_report().page_url, "chrome://bookmarks/add");
}
TEST_F(WebUIMainFrameObserverTest, ErrorsNotReportedInOtherFrames) {
NavigateToPage();
auto another_contents =
TestWebContents::Create(browser_context(), site_instance_);
CHECK(another_contents->GetPrimaryMainFrame());
CallOnDidAddMessageToConsole(another_contents->GetPrimaryMainFrame(),
blink::mojom::ConsoleMessageLevel::kError,
kMessage16, 5, kSourceURL16, kStackTrace16);
task_environment()->RunUntilIdle();
EXPECT_EQ(processor_->error_report_count(), 0);
}
TEST_F(WebUIMainFrameObserverTest, ErrorsNotReportedForNonChromeURLs) {
NavigateToPage();
const char16_t* const kNonChromeSourceURLs[] = {
u"chrome-untrusted://media-app",
u"chrome-error://chromewebdata/",
u"chrome-extension://abc123/",
u"about:blank",
};
for (const auto* url : kNonChromeSourceURLs) {
CallOnDidAddMessageToConsole(web_ui_->GetRenderFrameHost(),
blink::mojom::ConsoleMessageLevel::kError,
kMessage16, 5, url, kStackTrace16);
task_environment()->RunUntilIdle();
EXPECT_EQ(processor_->error_report_count(), 0) << url;
}
}
} // namespace content
#endif // BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)