blob: 9d7bad44bfd1345ecbd1e469432e2b7904c645be [file] [log] [blame]
// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/containers/contains.h"
#include "base/containers/span.h"
#include "base/functional/bind.h"
#include "base/memory/ref_counted_memory.h"
#include "base/strings/string_view_util.h"
#include "base/strings/utf_string_conversions.h"
#include "content/browser/webui/web_ui_data_source_impl.h"
#include "content/public/common/buildflags.h"
#include "content/public/test/browser_task_environment.h"
#include "content/test/test_content_client.h"
#include "services/network/public/mojom/content_security_policy.mojom.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
#if BUILDFLAG(LOAD_WEBUI_FROM_DISK)
#include "base/command_line.h"
#include "content/public/common/content_switches.h"
#include "ui/base/webui/resource_path.h"
#endif
namespace content {
namespace {
const int kDummyStringId = 123;
const int kDummyDefaultResourceId = 456;
const int kDummyResourceId = 789;
const int kDummyJSResourceId = 790;
const char16_t kDummyString[] = u"foo";
const char kDummyDefaultResource[] = "<html>foo</html>";
const char kDummyResource[] = "<html>blah</html>";
const char kDummyJSResource[] = "export const bar = 5;";
class TestClient : public TestContentClient {
public:
~TestClient() override {}
std::u16string GetLocalizedString(int message_id) override {
if (message_id == kDummyStringId)
return kDummyString;
return std::u16string();
}
base::RefCountedMemory* GetDataResourceBytes(int resource_id) override {
base::RefCountedStaticMemory* bytes = nullptr;
if (resource_id == kDummyDefaultResourceId) {
bytes = new base::RefCountedStaticMemory(
base::byte_span_with_nul_from_cstring(kDummyDefaultResource));
} else if (resource_id == kDummyResourceId) {
bytes = new base::RefCountedStaticMemory(
base::byte_span_with_nul_from_cstring(kDummyResource));
} else if (resource_id == kDummyJSResourceId) {
bytes = new base::RefCountedStaticMemory(
base::byte_span_with_nul_from_cstring(kDummyJSResource));
}
return bytes;
}
};
} // namespace
class WebUIDataSourceTest : public testing::Test {
public:
WebUIDataSourceTest() {}
~WebUIDataSourceTest() override {}
WebUIDataSourceImpl* source() { return source_.get(); }
void StartDataRequest(const std::string& path,
URLDataSource::GotDataCallback callback) {
source_->StartDataRequest(GURL("https://any-host/" + path),
WebContents::Getter(), std::move(callback));
}
std::string GetMimeTypeForPath(const std::string& path) const {
return source_->GetMimeType(GURL("https://any-host/" + path));
}
void HandleRequest(const std::string& path,
WebUIDataSourceImpl::GotDataCallback) {
request_path_ = path;
}
void RequestFilterQueryStringCallback(
scoped_refptr<base::RefCountedMemory> data);
protected:
std::string request_path_;
TestClient client_;
void CreateDataSource(std::string source_name) {
WebUIDataSourceImpl* source = new WebUIDataSourceImpl(source_name);
source->disable_load_time_data_defaults_for_testing();
source_ = base::WrapRefCounted(source);
}
void SetUp() override {
SetContentClient(&client_);
CreateDataSource("host");
}
private:
BrowserTaskEnvironment task_environment_;
scoped_refptr<WebUIDataSourceImpl> source_;
};
void EmptyStringsCallback(bool from_js_module,
scoped_refptr<base::RefCountedMemory> data) {
std::string result(base::as_string_view(*data));
EXPECT_NE(result.find("loadTimeData.data = {"), std::string::npos);
EXPECT_NE(result.find("};"), std::string::npos);
bool has_import = result.find("import {loadTimeData}") != std::string::npos;
EXPECT_EQ(from_js_module, has_import);
}
TEST_F(WebUIDataSourceTest, EmptyStrings) {
source()->UseStringsJs();
StartDataRequest("strings.js", base::BindOnce(&EmptyStringsCallback, false));
}
TEST_F(WebUIDataSourceTest, EmptyModuleStrings) {
source()->UseStringsJs();
StartDataRequest("strings.m.js", base::BindOnce(&EmptyStringsCallback, true));
}
void SomeValuesCallback(scoped_refptr<base::RefCountedMemory> data) {
std::string result(base::as_string_view(*data));
EXPECT_NE(result.find("\"flag\":true"), std::string::npos);
EXPECT_NE(result.find("\"counter\":10"), std::string::npos);
EXPECT_NE(result.find("\"debt\":-456"), std::string::npos);
EXPECT_NE(result.find("\"threshold\":0.55"), std::string::npos);
EXPECT_NE(result.find("\"planet\":\"pluto\""), std::string::npos);
EXPECT_NE(result.find("\"button\":\"foo\""), std::string::npos);
}
TEST_F(WebUIDataSourceTest, SomeValues) {
source()->UseStringsJs();
source()->AddBoolean("flag", true);
source()->AddInteger("counter", 10);
source()->AddInteger("debt", -456);
source()->AddDouble("threshold", 0.55);
source()->AddString("planet", u"pluto");
source()->AddLocalizedString("button", kDummyStringId);
StartDataRequest("strings.js", base::BindOnce(&SomeValuesCallback));
}
void DefaultResourceFoobarCallback(scoped_refptr<base::RefCountedMemory> data) {
std::string result(base::as_string_view(*data));
EXPECT_NE(result.find(kDummyDefaultResource), std::string::npos);
}
void DefaultResourceStringsCallback(
scoped_refptr<base::RefCountedMemory> data) {
std::string result(base::as_string_view(*data));
EXPECT_NE(result.find(kDummyDefaultResource), std::string::npos);
}
TEST_F(WebUIDataSourceTest, DefaultResource) {
source()->SetDefaultResource(kDummyDefaultResourceId);
StartDataRequest("foobar", base::BindOnce(&DefaultResourceFoobarCallback));
StartDataRequest("strings.js",
base::BindOnce(&DefaultResourceStringsCallback));
}
void NamedResourceFoobarCallback(scoped_refptr<base::RefCountedMemory> data) {
std::string result(base::as_string_view(*data));
EXPECT_NE(result.find(kDummyResource), std::string::npos);
}
void NamedResourceStringsCallback(scoped_refptr<base::RefCountedMemory> data) {
std::string result(base::as_string_view(*data));
EXPECT_NE(result.find(kDummyDefaultResource), std::string::npos);
}
TEST_F(WebUIDataSourceTest, NamedResource) {
source()->SetDefaultResource(kDummyDefaultResourceId);
source()->AddResourcePath("foobar", kDummyResourceId);
StartDataRequest("foobar", base::BindOnce(&NamedResourceFoobarCallback));
StartDataRequest("strings.js", base::BindOnce(&NamedResourceStringsCallback));
}
void NamedResourceWithQueryStringCallback(
scoped_refptr<base::RefCountedMemory> data) {
std::string result(base::as_string_view(*data));
EXPECT_NE(result.find(kDummyResource), std::string::npos);
}
TEST_F(WebUIDataSourceTest, NamedResourceWithQueryString) {
source()->SetDefaultResource(kDummyDefaultResourceId);
source()->AddResourcePath("foobar", kDummyResourceId);
StartDataRequest("foobar?query?string",
base::BindOnce(&NamedResourceWithQueryStringCallback));
}
void NamedResourceWithUrlFragmentCallback(
scoped_refptr<base::RefCountedMemory> data) {
EXPECT_NE(data, nullptr);
std::string result(base::as_string_view(*data));
EXPECT_NE(result.find(kDummyResource), std::string::npos);
}
TEST_F(WebUIDataSourceTest, NamedResourceWithUrlFragment) {
source()->AddResourcePath("foobar", kDummyResourceId);
StartDataRequest("foobar#fragment",
base::BindOnce(&NamedResourceWithUrlFragmentCallback));
}
void WebUIDataSourceTest::RequestFilterQueryStringCallback(
scoped_refptr<base::RefCountedMemory> data) {
std::string result(base::as_string_view(*data));
// Check that the query string is passed to the request filter (and not
// trimmed).
EXPECT_EQ("foobar?query?string", request_path_);
}
TEST_F(WebUIDataSourceTest, RequestFilterQueryString) {
request_path_ = std::string();
source()->SetRequestFilter(
base::BindRepeating([](const std::string& path) { return true; }),
base::BindRepeating(&WebUIDataSourceTest::HandleRequest,
base::Unretained(this)));
source()->SetDefaultResource(kDummyDefaultResourceId);
source()->AddResourcePath("foobar", kDummyResourceId);
StartDataRequest(
"foobar?query?string",
base::BindOnce(&WebUIDataSourceTest::RequestFilterQueryStringCallback,
base::Unretained(this)));
}
TEST_F(WebUIDataSourceTest, MimeType) {
const char* css = "text/css";
const char* html = "text/html";
const char* js = "application/javascript";
const char* jpg = "image/jpeg";
const char* json = "application/json";
const char* mp4 = "video/mp4";
const char* pdf = "application/pdf";
const char* png = "image/png";
const char* svg = "image/svg+xml";
const char* wasm = "application/wasm";
const char* woff2 = "application/font-woff2";
EXPECT_EQ(GetMimeTypeForPath(std::string()), html);
EXPECT_EQ(GetMimeTypeForPath("foo"), html);
EXPECT_EQ(GetMimeTypeForPath("foo.html"), html);
EXPECT_EQ(GetMimeTypeForPath(".js"), js);
EXPECT_EQ(GetMimeTypeForPath("foo.js"), js);
EXPECT_EQ(GetMimeTypeForPath("js"), html);
EXPECT_EQ(GetMimeTypeForPath("foojs"), html);
EXPECT_EQ(GetMimeTypeForPath("foo.jsp"), html);
EXPECT_EQ(GetMimeTypeForPath("foocss"), html);
EXPECT_EQ(GetMimeTypeForPath("foo.css"), css);
EXPECT_EQ(GetMimeTypeForPath(".css.foo"), html);
EXPECT_EQ(GetMimeTypeForPath("foopng"), html);
EXPECT_EQ(GetMimeTypeForPath("foo.png"), png);
EXPECT_EQ(GetMimeTypeForPath(".png.foo"), html);
EXPECT_EQ(GetMimeTypeForPath("foo.svg"), svg);
EXPECT_EQ(GetMimeTypeForPath("foo.js.wasm"), wasm);
EXPECT_EQ(GetMimeTypeForPath("foo.out.wasm"), wasm);
EXPECT_EQ(GetMimeTypeForPath(".woff2"), woff2);
EXPECT_EQ(GetMimeTypeForPath("foo.json"), json);
EXPECT_EQ(GetMimeTypeForPath("foo.pdf"), pdf);
EXPECT_EQ(GetMimeTypeForPath("foo.jpg"), jpg);
EXPECT_EQ(GetMimeTypeForPath("foo.mp4"), mp4);
// With query strings.
EXPECT_EQ(GetMimeTypeForPath("foo?abc?abc"), html);
EXPECT_EQ(GetMimeTypeForPath("foo.html?abc?abc"), html);
EXPECT_EQ(GetMimeTypeForPath("foo.css?abc?abc"), css);
EXPECT_EQ(GetMimeTypeForPath("foo.js?abc?abc"), js);
EXPECT_EQ(GetMimeTypeForPath("foo.svg?abc?abc"), svg);
// With URL fragments.
EXPECT_EQ(GetMimeTypeForPath("foo#abc#abc"), html);
EXPECT_EQ(GetMimeTypeForPath("foo.html#abc#abc"), html);
EXPECT_EQ(GetMimeTypeForPath("foo.css#abc#abc"), css);
EXPECT_EQ(GetMimeTypeForPath("foo.js#abc#abc"), js);
EXPECT_EQ(GetMimeTypeForPath("foo.svg#abc#abc"), svg);
// With query strings and URL fragments.
EXPECT_EQ(GetMimeTypeForPath("foo?abc#abc"), html);
EXPECT_EQ(GetMimeTypeForPath("foo.html?abc#abc"), html);
EXPECT_EQ(GetMimeTypeForPath("foo.css?abc#abc"), css);
EXPECT_EQ(GetMimeTypeForPath("foo.js?abc#abc"), js);
EXPECT_EQ(GetMimeTypeForPath("foo.svg?abc#abc"), svg);
}
TEST_F(WebUIDataSourceTest, ShouldServeMimeTypeAsContentTypeHeader) {
EXPECT_TRUE(source()->source()->ShouldServeMimeTypeAsContentTypeHeader());
}
void InvalidResourceCallback(scoped_refptr<base::RefCountedMemory> data) {
EXPECT_EQ(nullptr, data);
}
void NamedResourceBarJSCallback(scoped_refptr<base::RefCountedMemory> data) {
std::string result(base::as_string_view(*data));
EXPECT_NE(result.find(kDummyJSResource), std::string::npos);
}
TEST_F(WebUIDataSourceTest, NoSetDefaultResource) {
// Set an empty path resource instead of a default.
source()->AddResourcePath("", kDummyDefaultResourceId);
source()->AddResourcePath("foobar.html", kDummyResourceId);
source()->AddResourcePath("bar.js", kDummyJSResourceId);
// Empty paths return the resource for the empty path.
StartDataRequest("", base::BindOnce(&DefaultResourceFoobarCallback));
StartDataRequest("/", base::BindOnce(&DefaultResourceFoobarCallback));
// Un-mapped path that does not look like a file request also returns the
// resource associated with the empty path.
StartDataRequest("subpage", base::BindOnce(&DefaultResourceFoobarCallback));
// Paths that are valid filenames succeed and return the file contents.
StartDataRequest("foobar.html", base::BindOnce(&NamedResourceFoobarCallback));
StartDataRequest("bar.js", base::BindOnce(&NamedResourceBarJSCallback));
// Invalid file requests fail
StartDataRequest("does_not_exist.html",
base::BindOnce(&InvalidResourceCallback));
StartDataRequest("does_not_exist.js",
base::BindOnce(&InvalidResourceCallback));
StartDataRequest("does_not_exist.ts",
base::BindOnce(&InvalidResourceCallback));
// strings.m.js fails until UseStringsJs is called.
StartDataRequest("strings.m.js", base::BindOnce(&InvalidResourceCallback));
source()->UseStringsJs();
StartDataRequest("strings.m.js", base::BindOnce(&EmptyStringsCallback, true));
}
TEST_F(WebUIDataSourceTest, SetCspValues) {
URLDataSource* url_data_source = source()->source();
// Default values.
EXPECT_EQ("child-src 'none';",
url_data_source->GetContentSecurityPolicy(
network::mojom::CSPDirectiveName::ChildSrc));
EXPECT_EQ("", url_data_source->GetContentSecurityPolicy(
network::mojom::CSPDirectiveName::ConnectSrc));
EXPECT_EQ("", url_data_source->GetContentSecurityPolicy(
network::mojom::CSPDirectiveName::DefaultSrc));
EXPECT_EQ("", url_data_source->GetContentSecurityPolicy(
network::mojom::CSPDirectiveName::FrameSrc));
EXPECT_EQ("", url_data_source->GetContentSecurityPolicy(
network::mojom::CSPDirectiveName::ImgSrc));
EXPECT_EQ("", url_data_source->GetContentSecurityPolicy(
network::mojom::CSPDirectiveName::MediaSrc));
EXPECT_EQ("object-src 'none';",
url_data_source->GetContentSecurityPolicy(
network::mojom::CSPDirectiveName::ObjectSrc));
EXPECT_EQ("script-src chrome://resources 'self';",
url_data_source->GetContentSecurityPolicy(
network::mojom::CSPDirectiveName::ScriptSrc));
EXPECT_EQ("", url_data_source->GetContentSecurityPolicy(
network::mojom::CSPDirectiveName::StyleSrc));
EXPECT_EQ("require-trusted-types-for 'script';",
url_data_source->GetContentSecurityPolicy(
network::mojom::CSPDirectiveName::RequireTrustedTypesFor));
EXPECT_EQ("trusted-types;",
url_data_source->GetContentSecurityPolicy(
network::mojom::CSPDirectiveName::TrustedTypes));
// Override each directive and test it updates the underlying URLDataSource.
source()->OverrideContentSecurityPolicy(
network::mojom::CSPDirectiveName::ChildSrc, "child-src 'self';");
EXPECT_EQ("child-src 'self';",
url_data_source->GetContentSecurityPolicy(
network::mojom::CSPDirectiveName::ChildSrc));
source()->OverrideContentSecurityPolicy(
network::mojom::CSPDirectiveName::ConnectSrc,
"connect-src 'self' 'unsafe-inline';");
EXPECT_EQ("connect-src 'self' 'unsafe-inline';",
url_data_source->GetContentSecurityPolicy(
network::mojom::CSPDirectiveName::ConnectSrc));
source()->OverrideContentSecurityPolicy(
network::mojom::CSPDirectiveName::DefaultSrc, "default-src 'self';");
EXPECT_EQ("default-src 'self';",
url_data_source->GetContentSecurityPolicy(
network::mojom::CSPDirectiveName::DefaultSrc));
source()->OverrideContentSecurityPolicy(
network::mojom::CSPDirectiveName::FrameSrc, "frame-src 'self';");
EXPECT_EQ("frame-src 'self';",
url_data_source->GetContentSecurityPolicy(
network::mojom::CSPDirectiveName::FrameSrc));
source()->OverrideContentSecurityPolicy(
network::mojom::CSPDirectiveName::ImgSrc, "img-src 'self' blob:;");
EXPECT_EQ("img-src 'self' blob:;",
url_data_source->GetContentSecurityPolicy(
network::mojom::CSPDirectiveName::ImgSrc));
source()->OverrideContentSecurityPolicy(
network::mojom::CSPDirectiveName::MediaSrc, "media-src 'self' blob:;");
EXPECT_EQ("media-src 'self' blob:;",
url_data_source->GetContentSecurityPolicy(
network::mojom::CSPDirectiveName::MediaSrc));
source()->OverrideContentSecurityPolicy(
network::mojom::CSPDirectiveName::ObjectSrc, "object-src 'self' data:;");
EXPECT_EQ("object-src 'self' data:;",
url_data_source->GetContentSecurityPolicy(
network::mojom::CSPDirectiveName::ObjectSrc));
source()->OverrideContentSecurityPolicy(
network::mojom::CSPDirectiveName::ScriptSrc,
"script-src chrome://resources 'self' 'unsafe-inline';");
EXPECT_EQ("script-src chrome://resources 'self' 'unsafe-inline';",
url_data_source->GetContentSecurityPolicy(
network::mojom::CSPDirectiveName::ScriptSrc));
source()->OverrideContentSecurityPolicy(
network::mojom::CSPDirectiveName::StyleSrc,
"style-src 'self' 'unsafe-inline';");
EXPECT_EQ("style-src 'self' 'unsafe-inline';",
url_data_source->GetContentSecurityPolicy(
network::mojom::CSPDirectiveName::StyleSrc));
source()->OverrideContentSecurityPolicy(
network::mojom::CSPDirectiveName::RequireTrustedTypesFor,
"require-trusted-types-for 'wasm';");
EXPECT_EQ("require-trusted-types-for 'wasm';",
url_data_source->GetContentSecurityPolicy(
network::mojom::CSPDirectiveName::RequireTrustedTypesFor));
source()->OverrideContentSecurityPolicy(
network::mojom::CSPDirectiveName::TrustedTypes, "trusted-types test;");
EXPECT_EQ("trusted-types test;",
url_data_source->GetContentSecurityPolicy(
network::mojom::CSPDirectiveName::TrustedTypes));
source()->DisableTrustedTypesCSP();
EXPECT_EQ("", url_data_source->GetContentSecurityPolicy(
network::mojom::CSPDirectiveName::RequireTrustedTypesFor));
EXPECT_EQ("", url_data_source->GetContentSecurityPolicy(
network::mojom::CSPDirectiveName::TrustedTypes));
}
TEST_F(WebUIDataSourceTest, SetCrossOriginPolicyValues) {
URLDataSource* url_data_source = source()->source();
// Default values.
EXPECT_EQ("", url_data_source->GetCrossOriginOpenerPolicy());
EXPECT_EQ("", url_data_source->GetCrossOriginEmbedderPolicy());
EXPECT_EQ("", url_data_source->GetCrossOriginResourcePolicy());
// Overridden values.
source()->OverrideCrossOriginOpenerPolicy("same-origin");
EXPECT_EQ("same-origin", url_data_source->GetCrossOriginOpenerPolicy());
source()->OverrideCrossOriginEmbedderPolicy("require-corp");
EXPECT_EQ("require-corp", url_data_source->GetCrossOriginEmbedderPolicy());
source()->OverrideCrossOriginResourcePolicy("cross-origin");
EXPECT_EQ("cross-origin", url_data_source->GetCrossOriginResourcePolicy());
// Remove/change the values.
source()->OverrideCrossOriginOpenerPolicy("same-site");
EXPECT_EQ("same-site", url_data_source->GetCrossOriginOpenerPolicy());
source()->OverrideCrossOriginEmbedderPolicy("");
EXPECT_EQ("", url_data_source->GetCrossOriginEmbedderPolicy());
source()->OverrideCrossOriginResourcePolicy("same-origin");
EXPECT_EQ("same-origin", url_data_source->GetCrossOriginResourcePolicy());
}
TEST_F(WebUIDataSourceTest, GetOrigin) {
CreateDataSource("host");
EXPECT_EQ(source()->GetOrigin(), url::Origin::Create(GURL("chrome://host")));
CreateDataSource("chrome-untrusted://host/");
EXPECT_EQ(source()->GetOrigin(),
url::Origin::Create(GURL("chrome-untrusted://host")));
}
#if BUILDFLAG(LOAD_WEBUI_FROM_DISK)
// LoadWebUIFromDiskTest does not run on any bots, only meant to run locally,
// since it tests a feature only used during local development and guarded by a
// build flag.
class LoadWebUIFromDiskTest : public WebUIDataSourceTest {
protected:
void SetUp() override {
base::CommandLine::ForCurrentProcess()->AppendSwitch(
::switches::kLoadWebUIfromDisk);
WebUIDataSourceTest::SetUp();
}
};
void LoadFromDiskCallback(scoped_refptr<base::RefCountedMemory> data) {
std::string result(base::as_string_view(*data));
EXPECT_TRUE(base::Contains(result, "hello plain!"));
}
TEST_F(LoadWebUIFromDiskTest, FilepathInfoExists) {
// The path must be relative to DIR_EXE.
base::FilePath path =
base::FilePath::FromUTF8Unsafe("../../content/test/data/plain.txt");
const webui::ResourcePath kResources[1] = {
{
"bar.js",
kDummyJSResourceId,
path.value().c_str(),
},
};
source()->AddResourcePaths(kResources);
StartDataRequest("bar.js", base::BindOnce(&LoadFromDiskCallback));
}
TEST_F(LoadWebUIFromDiskTest, FilepathInfoNotExists) {
const webui::ResourcePath kResources[1] = {
{"bar.js", kDummyJSResourceId},
};
source()->AddResourcePaths(kResources);
StartDataRequest("bar.js", base::BindOnce(&NamedResourceBarJSCallback));
}
#endif // BUILDFLAG(LOAD_WEBUI_FROM_DISK)
} // namespace content