blob: 1aac0d9965a563aa39d9fffb0ddd1124e245bdf1 [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 "content/browser/webui/web_ui_data_source_impl.h"
#include <stddef.h>
#include <stdint.h>
#include <memory>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include "base/check.h"
#include "base/check_op.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/ref_counted_memory.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/thread_pool.h"
#include "base/trace_event/trace_event.h"
#include "base/values.h"
#include "content/grit/content_resources.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/common/buildflags.h"
#include "content/public/common/content_client.h"
#include "content/public/common/url_constants.h"
#include "services/network/public/mojom/content_security_policy.mojom.h"
#include "ui/base/template_expressions.h"
#include "ui/base/webui/jstemplate_builder.h"
#include "ui/base/webui/resource_path.h"
#include "ui/base/webui/web_ui_util.h"
#include "url/origin.h"
#if BUILDFLAG(LOAD_WEBUI_FROM_DISK)
#include "base/base_paths.h"
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/path_service.h"
#include "content/public/common/content_switches.h"
#if BUILDFLAG(IS_MAC)
#include "base/apple/foundation_util.h"
#endif // BUILDFLAG(IS_MAC)
#endif // BUILDFLAG(LOAD_WEBUI_FROM_DISK)
namespace content {
// static
WebUIDataSource* WebUIDataSource::CreateAndAdd(BrowserContext* browser_context,
const std::string& source_name) {
auto* data_source = new WebUIDataSourceImpl(source_name);
URLDataManager::AddWebUIDataSource(browser_context, data_source);
return data_source;
}
// static
void WebUIDataSource::Update(BrowserContext* browser_context,
const std::string& source_name,
const base::Value::Dict& update) {
URLDataManager::UpdateWebUIDataSource(browser_context, source_name,
std::move(update));
}
namespace {
void GetDataResourceBytesOnWorkerThread(
int resource_id,
URLDataSource::GotDataCallback callback) {
base::ThreadPool::CreateSequencedTaskRunner(
{base::TaskPriority::USER_BLOCKING,
base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN, base::MayBlock()})
->PostTask(
FROM_HERE,
base::BindOnce(
[](int resource_id, URLDataSource::GotDataCallback callback) {
ContentClient* content_client = GetContentClient();
DCHECK(content_client);
std::move(callback).Run(
content_client->GetDataResourceBytes(resource_id));
},
resource_id, std::move(callback)));
}
#if BUILDFLAG(LOAD_WEBUI_FROM_DISK)
void GetDataResourceBytesOnWorkerThreadFromDisk(
std::string filepath,
URLDataSource::GotDataCallback callback) {
base::ThreadPool::CreateSequencedTaskRunner(
{base::TaskPriority::USER_BLOCKING,
base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN, base::MayBlock()})
->PostTask(
FROM_HERE,
base::BindOnce(
[](std::string filepath,
URLDataSource::GotDataCallback callback) {
base::FilePath file_path;
base::PathService::Get(base::DIR_EXE, &file_path);
#if BUILDFLAG(IS_MAC)
if (base::apple::AmIBundled()) {
file_path = file_path.Append(FILE_PATH_LITERAL("../../.."));
}
#endif
file_path =
file_path.Append(base::FilePath::FromUTF8Unsafe(filepath))
.NormalizePathSeparators();
if (file_path.ReferencesParent()) {
// Convert to absolute, otherwise ReadFileToString()
// will refuse to load the file.
base::FilePath absolute_file_path =
base::MakeAbsoluteFilePath(file_path);
file_path = absolute_file_path;
}
std::string content;
const bool ok = base::ReadFileToString(file_path, &content);
CHECK(ok) << "Failed to load from disk: " << filepath;
scoped_refptr<base::RefCountedString> response =
base::MakeRefCounted<base::RefCountedString>(
std::move(content));
std::move(callback).Run(response.get());
},
filepath, std::move(callback)));
}
#endif // BUILDFLAG(LOAD_WEBUI_FROM_DISK)
const int kNonExistentResource = -1;
} // namespace
// Internal class to hide the fact that WebUIDataSourceImpl implements
// URLDataSource.
class WebUIDataSourceImpl::InternalDataSource : public URLDataSource {
public:
explicit InternalDataSource(WebUIDataSourceImpl* parent) : parent_(parent) {}
~InternalDataSource() override {}
// URLDataSource implementation.
std::string GetSource() override { return parent_->GetSource(); }
std::string GetMimeType(const GURL& url) override {
return parent_->GetMimeType(url);
}
void StartDataRequest(const GURL& url,
const WebContents::Getter& wc_getter,
URLDataSource::GotDataCallback callback) override {
return parent_->StartDataRequest(url, wc_getter, std::move(callback));
}
bool AllowCaching() override { return false; }
std::string GetContentSecurityPolicy(
network::mojom::CSPDirectiveName directive) override {
if (parent_->csp_overrides_.contains(directive)) {
return parent_->csp_overrides_.at(directive);
} else if (directive == network::mojom::CSPDirectiveName::FrameAncestors) {
std::string frame_ancestors;
if (parent_->frame_ancestors_.size() == 0)
frame_ancestors += " 'none'";
for (const GURL& frame_ancestor : parent_->frame_ancestors_) {
frame_ancestors += " " + frame_ancestor.spec();
}
return "frame-ancestors" + frame_ancestors + ";";
}
return URLDataSource::GetContentSecurityPolicy(directive);
}
std::string GetCrossOriginOpenerPolicy() override {
return parent_->coop_value_;
}
std::string GetCrossOriginEmbedderPolicy() override {
return parent_->coep_value_;
}
std::string GetCrossOriginResourcePolicy() override {
return parent_->corp_value_;
}
bool ShouldDenyXFrameOptions() override {
return parent_->deny_xframe_options_;
}
bool ShouldServeMimeTypeAsContentTypeHeader() override { return true; }
const ui::TemplateReplacements* GetReplacements() override {
return &parent_->replacements_;
}
bool ShouldReplaceI18nInJS() override {
return parent_->ShouldReplaceI18nInJS();
}
bool ShouldServiceRequest(const GURL& url,
BrowserContext* browser_context,
int render_process_id) override {
if (parent_->supported_scheme_.has_value()) {
return url.SchemeIs(parent_->supported_scheme_.value());
}
return URLDataSource::ShouldServiceRequest(url, browser_context,
render_process_id);
}
private:
raw_ptr<WebUIDataSourceImpl> parent_;
};
WebUIDataSourceImpl::WebUIDataSourceImpl(const std::string& source_name)
: URLDataSourceImpl(source_name,
std::make_unique<InternalDataSource>(this)),
source_name_(source_name),
default_resource_(kNonExistentResource) {
// |source_name| is assumed to match one of the following patterns:
//
// some-host
// chrome-untrusted://some-host/
// some-scheme://
//
// Source names of the form "some-scheme://" are explicitly disallowed.
CHECK(!source_name.ends_with("://"));
#if BUILDFLAG(LOAD_WEBUI_FROM_DISK)
load_from_disk_ = base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kLoadWebUIfromDisk);
#endif // BUILDFLAG(LOAD_WEBUI_FROM_DISK)
}
WebUIDataSourceImpl::~WebUIDataSourceImpl() = default;
void WebUIDataSourceImpl::AddString(std::string_view name,
std::u16string_view value) {
// TODO(dschuyler): Share only one copy of these strings.
localized_strings_.Set(name, value);
replacements_[std::string(name)] = base::UTF16ToUTF8(value);
}
void WebUIDataSourceImpl::AddString(std::string_view name,
std::string_view value) {
localized_strings_.Set(name, value);
replacements_[std::string(name)] = value;
}
void WebUIDataSourceImpl::AddLocalizedString(std::string_view name, int ids) {
std::string utf8_str =
base::UTF16ToUTF8(GetContentClient()->GetLocalizedString(ids));
localized_strings_.Set(name, utf8_str);
replacements_[std::string(name)] = utf8_str;
}
void WebUIDataSourceImpl::AddLocalizedStrings(
base::span<const webui::LocalizedString> strings) {
for (const auto& str : strings)
AddLocalizedString(str.name, str.id);
}
void WebUIDataSourceImpl::AddLocalizedStrings(
const base::Value::Dict& localized_strings) {
localized_strings_.Merge(localized_strings.Clone());
ui::TemplateReplacementsFromDictionaryValue(localized_strings,
&replacements_);
}
void WebUIDataSourceImpl::AddBoolean(std::string_view name, bool value) {
localized_strings_.Set(name, value);
// TODO(dschuyler): Change name of |localized_strings_| to |load_time_data_|
// or similar. These values haven't been found as strings for
// localization. The boolean values are not added to |replacements_|
// for the same reason, that they are used as flags, rather than string
// replacements.
}
void WebUIDataSourceImpl::AddInteger(std::string_view name, int32_t value) {
localized_strings_.Set(name, value);
}
void WebUIDataSourceImpl::AddDouble(std::string_view name, double value) {
localized_strings_.Set(name, value);
}
void WebUIDataSourceImpl::UseStringsJs() {
use_strings_js_ = true;
}
void WebUIDataSourceImpl::AddResourcePath(std::string_view path,
int resource_id) {
path_to_idr_map_[std::string(path)] = resource_id;
}
void WebUIDataSourceImpl::AddResourcePaths(
base::span<const webui::ResourcePath> paths) {
for (const auto& resource : paths) {
AddResourcePath(resource.path, resource.id);
#if BUILDFLAG(LOAD_WEBUI_FROM_DISK)
if (load_from_disk_ && resource.filepath.has_value()) {
CHECK(strlen(resource.filepath.value()) > 0u);
idr_to_file_map_[resource.id] = resource.filepath.value();
}
#endif // BUILDFLAG(LOAD_WEBUI_FROM_DISK)
}
}
void WebUIDataSourceImpl::SetDefaultResource(int resource_id) {
default_resource_ = resource_id;
}
void WebUIDataSourceImpl::SetRequestFilter(
const ShouldHandleRequestCallback& should_handle_request_callback,
const HandleRequestCallback& handle_request_callback) {
CHECK(!should_handle_request_callback_);
CHECK(!filter_callback_);
should_handle_request_callback_ = should_handle_request_callback;
filter_callback_ = handle_request_callback;
}
bool WebUIDataSourceImpl::IsWebUIDataSourceImpl() const {
return true;
}
void WebUIDataSourceImpl::OverrideContentSecurityPolicy(
network::mojom::CSPDirectiveName directive,
const std::string& value) {
csp_overrides_.insert_or_assign(directive, value);
}
void WebUIDataSourceImpl::OverrideCrossOriginOpenerPolicy(
const std::string& value) {
coop_value_ = value;
}
void WebUIDataSourceImpl::OverrideCrossOriginEmbedderPolicy(
const std::string& value) {
coep_value_ = value;
}
void WebUIDataSourceImpl::OverrideCrossOriginResourcePolicy(
const std::string& value) {
corp_value_ = value;
}
void WebUIDataSourceImpl::DisableTrustedTypesCSP() {
// TODO(crbug.com/40137141): Trusted Type remaining WebUI
// This removes require-trusted-types-for and trusted-types directives
// from the CSP header.
OverrideContentSecurityPolicy(
network::mojom::CSPDirectiveName::RequireTrustedTypesFor, std::string());
OverrideContentSecurityPolicy(network::mojom::CSPDirectiveName::TrustedTypes,
std::string());
}
void WebUIDataSourceImpl::AddFrameAncestor(const GURL& frame_ancestor) {
// Do not allow a wildcard to be a frame ancestor or it will allow any website
// to embed the WebUI.
CHECK(frame_ancestor.SchemeIs(kChromeUIScheme) ||
frame_ancestor.SchemeIs(kChromeUIUntrustedScheme));
frame_ancestors_.insert(frame_ancestor);
}
void WebUIDataSourceImpl::DisableDenyXFrameOptions() {
deny_xframe_options_ = false;
}
void WebUIDataSourceImpl::EnableReplaceI18nInJS() {
should_replace_i18n_in_js_ = true;
}
void WebUIDataSourceImpl::EnsureLoadTimeDataDefaultsAdded() {
if (!add_load_time_data_defaults_)
return;
add_load_time_data_defaults_ = false;
std::string locale = GetContentClient()->browser()->GetApplicationLocale();
base::Value::Dict defaults;
webui::SetLoadTimeDataDefaults(locale, &defaults);
AddLocalizedStrings(defaults);
}
std::string WebUIDataSourceImpl::GetSource() {
return source_name_;
}
url::Origin WebUIDataSourceImpl::GetOrigin() {
// |source_name_| is assumed to match one of the following patterns:
//
// some-host
// chrome-untrusted://some-host/
//
// so we use the GURL parser to differentiate the two cases.
url::Origin result;
GURL url(source_name_);
if (url.is_valid()) {
// |source_name_| must be of the form "chrome-untrusted://some-host/",
// which means it serves URLs of the form "chrome-untrusted://some-host/".
CHECK(url.SchemeIs(kChromeUIUntrustedScheme));
result = url::Origin::Create(url);
} else {
// |source_name_| must be of the form "some-host", which means it serves
// URLs of the form "chrome://some-host/".
result = url::Origin::CreateFromNormalizedTuple(kChromeUIScheme,
source_name_, 0);
}
CHECK(!result.opaque());
return result;
}
void WebUIDataSourceImpl::SetSupportedScheme(std::string_view scheme) {
CHECK(!supported_scheme_.has_value());
supported_scheme_ = scheme;
}
std::string WebUIDataSourceImpl::GetMimeType(const GURL& url) const {
const std::string_view file_path = url.path_piece();
if (base::EndsWith(file_path, ".css", base::CompareCase::INSENSITIVE_ASCII)) {
return "text/css";
}
if (base::EndsWith(file_path, ".js", base::CompareCase::INSENSITIVE_ASCII)) {
return "application/javascript";
}
if (base::EndsWith(file_path, ".ts", base::CompareCase::INSENSITIVE_ASCII)) {
return "application/typescript";
}
if (base::EndsWith(file_path, ".json",
base::CompareCase::INSENSITIVE_ASCII)) {
return "application/json";
}
if (base::EndsWith(file_path, ".pdf", base::CompareCase::INSENSITIVE_ASCII)) {
return "application/pdf";
}
if (base::EndsWith(file_path, ".svg", base::CompareCase::INSENSITIVE_ASCII)) {
return "image/svg+xml";
}
if (base::EndsWith(file_path, ".jpg", base::CompareCase::INSENSITIVE_ASCII)) {
return "image/jpeg";
}
if (base::EndsWith(file_path, ".png", base::CompareCase::INSENSITIVE_ASCII)) {
return "image/png";
}
if (base::EndsWith(file_path, ".mp4", base::CompareCase::INSENSITIVE_ASCII)) {
return "video/mp4";
}
if (base::EndsWith(file_path, ".wasm",
base::CompareCase::INSENSITIVE_ASCII)) {
return "application/wasm";
}
if (base::EndsWith(file_path, ".woff2",
base::CompareCase::INSENSITIVE_ASCII)) {
return "application/font-woff2";
}
return "text/html";
}
void WebUIDataSourceImpl::StartDataRequest(
const GURL& url,
const WebContents::Getter& wc_getter,
URLDataSource::GotDataCallback callback) {
const std::string path = URLDataSource::URLToRequestPath(url);
TRACE_EVENT1("ui", "WebUIDataSourceImpl::StartDataRequest", "path", path);
if (!should_handle_request_callback_.is_null() &&
should_handle_request_callback_.Run(path)) {
filter_callback_.Run(path, std::move(callback));
return;
}
EnsureLoadTimeDataDefaultsAdded();
if (use_strings_js_) {
bool from_js_module = path == "strings.m.js";
if (from_js_module || path == "strings.js") {
SendLocalizedStringsAsJSON(std::move(callback), from_js_module);
return;
}
}
int resource_id = URLToIdrOrDefault(url);
if (resource_id == kNonExistentResource) {
std::move(callback).Run(nullptr);
return;
}
#if BUILDFLAG(LOAD_WEBUI_FROM_DISK)
if (load_from_disk_) {
auto it = idr_to_file_map_.find(resource_id);
if (it != idr_to_file_map_.end()) {
GetDataResourceBytesOnWorkerThreadFromDisk(it->second,
std::move(callback));
return;
}
// Fall back to reading from the .pak file.
}
#endif // BUILDFLAG(LOAD_WEBUI_FROM_DISK)
GetDataResourceBytesOnWorkerThread(resource_id, std::move(callback));
}
void WebUIDataSourceImpl::SendLocalizedStringsAsJSON(
URLDataSource::GotDataCallback callback,
bool from_js_module) {
std::string template_data;
webui::AppendJsonJS(localized_strings_, &template_data, from_js_module);
std::move(callback).Run(
base::MakeRefCounted<base::RefCountedString>(std::move(template_data)));
}
const base::Value::Dict* WebUIDataSourceImpl::GetLocalizedStrings() const {
return &localized_strings_;
}
bool WebUIDataSourceImpl::ShouldReplaceI18nInJS() const {
return should_replace_i18n_in_js_;
}
int WebUIDataSourceImpl::URLToIdrOrDefault(const GURL& url) const {
const std::string path(url.path_piece().substr(1));
auto it = path_to_idr_map_.find(path);
if (it != path_to_idr_map_.end())
return it->second;
if (default_resource_ != kNonExistentResource)
return default_resource_;
// Use GetMimeType() to check for most file requests. It returns text/html by
// default regardless of the extension if it does not match a different file
// type, so check for HTML file requests separately.
if (GetMimeType(url) != "text/html" ||
base::EndsWith(path, ".html", base::CompareCase::INSENSITIVE_ASCII)) {
return kNonExistentResource;
}
// If not a file request, try to get the resource for the empty key.
auto it_empty = path_to_idr_map_.find("");
return (it_empty != path_to_idr_map_.end()) ? it_empty->second
: kNonExistentResource;
}
} // namespace content