blob: 78f5897a9d30b61ebd4c9122f3e99aa345b29786 [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 "chrome/browser/ui/webui/new_tab_page/untrusted_source.h"
#include <numeric>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include "base/base64.h"
#include "base/containers/contains.h"
#include "base/files/file_util.h"
#include "base/i18n/rtl.h"
#include "base/memory/ref_counted_memory.h"
#include "base/memory/scoped_refptr.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_util.h"
#include "base/strings/string_view_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/to_string.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "chrome/browser/new_tab_page/one_google_bar/one_google_bar_data.h"
#include "chrome/browser/new_tab_page/one_google_bar/one_google_bar_service_factory.h"
#include "chrome/browser/policy/chrome_policy_blocklist_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/search/background/ntp_custom_background_service.h"
#include "chrome/browser/search/background/ntp_custom_background_service_factory.h"
#include "chrome/browser/ui/search/ntp_user_data_logger.h"
#include "chrome/common/url_constants.h"
#include "chrome/grit/new_tab_page_untrusted_resources.h"
#include "components/policy/core/browser/url_list/policy_blocklist_service.h"
#include "components/search/ntp_features.h"
#include "content/public/common/url_constants.h"
#include "net/base/url_util.h"
#include "services/network/public/mojom/content_security_policy.mojom.h"
#include "third_party/re2/src/re2/re2.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/base/template_expressions.h"
#include "url/url_util.h"
using URLBlocklistState = policy::URLBlocklist::URLBlocklistState;
namespace {
constexpr int kMaxUriDecodeLen = 2048;
std::string FormatTemplate(int resource_id,
const ui::TemplateReplacements& replacements) {
ui::ResourceBundle& bundle = ui::ResourceBundle::GetSharedInstance();
scoped_refptr<base::RefCountedMemory> bytes =
bundle.LoadDataResourceBytes(resource_id);
return ui::ReplaceTemplateExpressions(
base::as_string_view(*bytes), replacements,
/* skip_unexpected_placeholder_check= */ true);
}
std::string ReadBackgroundImageData(const base::FilePath& path) {
std::string data_string;
base::ReadFileToString(path, &data_string);
return data_string;
}
void ServeBackgroundImageData(content::URLDataSource::GotDataCallback callback,
std::string data_string) {
std::move(callback).Run(
base::MakeRefCounted<base::RefCountedString>(std::move(data_string)));
}
std::string AsyncParamDataAsCSV(
const std::map<std::string, std::string>& param_data) {
if (param_data.empty()) {
return "";
}
std::string csv = std::accumulate(
param_data.cbegin(), param_data.cend(), std::string(),
[&](std::string acc, const std::pair<std::string, std::string>& param) {
return acc + "," + param.first + ":" + param.second;
});
// Strip preceding comma and return value.
return csv.substr(1);
}
std::map<std::string, std::string> ExtractQueryParams(
std::string_view query_params) {
std::map<std::string, std::string> params;
url::Component query(0, query_params.length());
url::Component key, value;
while (url::ExtractQueryKeyValue(query_params, &query, &key, &value)) {
url::RawCanonOutputW<kMaxUriDecodeLen> output;
url::DecodeURLEscapeSequences(query_params.substr(value.begin, value.len),
url::DecodeURLMode::kUTF8OrIsomorphic,
&output);
params.insert({std::string(query_params.substr(key.begin, key.len)),
base::UTF16ToUTF8(output.view())});
}
return params;
}
} // namespace
UntrustedSource::UntrustedSource(Profile* profile)
: one_google_bar_service_(
OneGoogleBarServiceFactory::GetForProfile(profile)),
profile_(profile) {
// |one_google_bar_service_| is null in incognito, or when the feature is
// disabled.
if (one_google_bar_service_) {
one_google_bar_service_observation_.Observe(one_google_bar_service_.get());
}
}
UntrustedSource::~UntrustedSource() = default;
std::string UntrustedSource::GetContentSecurityPolicy(
network::mojom::CSPDirectiveName directive) {
switch (directive) {
case network::mojom::CSPDirectiveName::ScriptSrc:
return "script-src 'self' 'unsafe-inline' https:;";
case network::mojom::CSPDirectiveName::ChildSrc:
return "child-src https:;";
case network::mojom::CSPDirectiveName::DefaultSrc:
// TODO(crbug.com/40693567): Audit and tighten CSP.
return std::string();
case network::mojom::CSPDirectiveName::FrameAncestors:
return base::StringPrintf("frame-ancestors %s",
chrome::kChromeUINewTabPageURL);
case network::mojom::CSPDirectiveName::RequireTrustedTypesFor:
return std::string();
case network::mojom::CSPDirectiveName::TrustedTypes:
return std::string();
case network::mojom::CSPDirectiveName::FormAction:
return "form-action https://ogs.google.com https://*.corp.google.com;";
default:
return content::URLDataSource::GetContentSecurityPolicy(directive);
}
}
std::string UntrustedSource::GetSource() {
return chrome::kChromeUIUntrustedNewTabPageUrl;
}
void UntrustedSource::StartDataRequest(
const GURL& url,
const content::WebContents::Getter& wc_getter,
content::URLDataSource::GotDataCallback callback) {
GURL url_param = GURL(url.GetQuery());
if (url_param.is_valid() && IsURLBlockedByPolicy(url_param)) {
std::move(callback).Run(base::MakeRefCounted<base::RefCountedString>());
return;
}
const std::string path = url.has_path() ? url.GetPath().substr(1) : "";
if (path == "one-google-bar" && one_google_bar_service_) {
std::map<std::string, std::string> params;
std::string query_params;
if (net::GetValueForKeyInQuery(url, "paramsencoded", &query_params)) {
base::Base64Decode(query_params, &query_params);
if (!query_params.empty() && query_params.starts_with("&")) {
params = ExtractQueryParams(query_params.substr(1));
}
}
if (params.find("async") == params.end()) {
std::map<std::string, std::string> async_param_data;
async_param_data["fixed"] = "0";
bool async_bar_parts = base::FeatureList::IsEnabled(
ntp_features::kNtpOneGoogleBarAsyncBarParts);
if (async_bar_parts) {
async_param_data["abp"] = "1";
NtpCustomBackgroundService* ntp_custom_background_service =
NtpCustomBackgroundServiceFactory::GetForProfile(profile_);
if (ntp_custom_background_service &&
ntp_custom_background_service->GetCustomBackground()) {
async_param_data["dark"] = "1";
} else {
content::WebContents* web_contents = wc_getter.Run();
if (web_contents && web_contents->GetColorMode() ==
ui::ColorProviderKey::ColorMode::kDark) {
async_param_data["dark"] = "1";
}
}
}
params.insert({"async", AsyncParamDataAsCSV(async_param_data)});
}
one_google_bar_service_->SetAdditionalQueryParams(params);
one_google_bar_callbacks_.push_back(std::move(callback));
if (one_google_bar_callbacks_.size() == 1) {
one_google_bar_load_start_time_ = base::TimeTicks::Now();
one_google_bar_service_->Refresh();
}
return;
}
if (path == "one_google_bar.js") {
ui::ResourceBundle& bundle = ui::ResourceBundle::GetSharedInstance();
std::move(callback).Run(bundle.LoadDataResourceBytes(
IDR_NEW_TAB_PAGE_UNTRUSTED_ONE_GOOGLE_BAR_JS));
return;
}
if (path == "one_google_bar_api.js") {
ui::ResourceBundle& bundle = ui::ResourceBundle::GetSharedInstance();
std::move(callback).Run(bundle.LoadDataResourceBytes(
IDR_NEW_TAB_PAGE_UNTRUSTED_ONE_GOOGLE_BAR_API_JS));
return;
}
// TODO(romanarora): add support for one_google_bar_api.js?
if (path == "image" && url_param.is_valid() &&
(url_param.SchemeIs(url::kHttpsScheme) ||
url_param.SchemeIs(content::kChromeUIUntrustedScheme))) {
ui::TemplateReplacements replacements;
replacements["url"] = url_param.spec();
std::move(callback).Run(base::MakeRefCounted<base::RefCountedString>(
FormatTemplate(IDR_NEW_TAB_PAGE_UNTRUSTED_IMAGE_HTML, replacements)));
return;
}
if (path == "background_image") {
ServeBackgroundImage(url_param, GURL(), "cover", "no-repeat", "no-repeat",
"center", "center", "inherit", std::move(callback));
return;
}
if (path == "custom_background_image") {
// Parse all query parameters to hash map and decode values.
std::map<std::string, std::string> params = ExtractQueryParams(url.query());
// Extract desired values.
ServeBackgroundImage(
params.count("url") == 1 ? GURL(params["url"]) : GURL(),
params.count("url2x") == 1 ? GURL(params["url2x"]) : GURL(),
params.count("size") == 1 ? params["size"] : "cover",
params.count("repeatX") == 1 ? params["repeatX"] : "no-repeat",
params.count("repeatY") == 1 ? params["repeatY"] : "no-repeat",
params.count("positionX") == 1 ? params["positionX"] : "center",
params.count("positionY") == 1 ? params["positionY"] : "center", "none",
std::move(callback));
return;
}
if (path == "background_image.js") {
ui::ResourceBundle& bundle = ui::ResourceBundle::GetSharedInstance();
std::move(callback).Run(bundle.LoadDataResourceBytes(
IDR_NEW_TAB_PAGE_UNTRUSTED_BACKGROUND_IMAGE_JS));
return;
}
if (base::EndsWith(path, "background.jpg")) {
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::TaskPriority::USER_VISIBLE, base::MayBlock()},
base::BindOnce(&ReadBackgroundImageData,
profile_->GetPath().AppendASCII(path)),
base::BindOnce(&ServeBackgroundImageData, std::move(callback)));
return;
}
std::move(callback).Run(base::MakeRefCounted<base::RefCountedString>());
}
std::string UntrustedSource::GetMimeType(const GURL& url) {
const std::string_view stripped_path = url.path();
if (base::EndsWith(stripped_path, ".js",
base::CompareCase::INSENSITIVE_ASCII)) {
return "application/javascript";
}
if (base::EndsWith(stripped_path, ".jpg",
base::CompareCase::INSENSITIVE_ASCII)) {
return "image/jpg";
}
return "text/html";
}
bool UntrustedSource::AllowCaching() {
return false;
}
bool UntrustedSource::ShouldReplaceExistingSource() {
return false;
}
bool UntrustedSource::ShouldServeMimeTypeAsContentTypeHeader() {
return true;
}
bool UntrustedSource::ShouldServiceRequest(
const GURL& url,
content::BrowserContext* browser_context,
int render_process_id) {
if (!url.SchemeIs(content::kChromeUIUntrustedScheme) || !url.has_path()) {
return false;
}
const std::string path = url.GetPath().substr(1);
return path == "one-google-bar" || path == "one_google_bar.js" ||
path == "one_google_bar_api.js" || path == "image" ||
path == "background_image" || path == "custom_background_image" ||
path == "background_image.js" ||
base::Contains(path, "background.jpg");
}
void UntrustedSource::OnOneGoogleBarDataUpdated() {
std::optional<OneGoogleBarData> data =
one_google_bar_service_->one_google_bar_data();
if (one_google_bar_load_start_time_.has_value()) {
NTPUserDataLogger::LogOneGoogleBarFetchDuration(
/*success=*/data.has_value(),
/*duration=*/base::TimeTicks::Now() - *one_google_bar_load_start_time_);
one_google_bar_load_start_time_ = std::nullopt;
}
std::string html;
if (data.has_value()) {
ui::TemplateReplacements replacements;
replacements["textdirection"] = base::i18n::IsRTL() ? "rtl" : "ltr";
replacements["barHtml"] = data->bar_html;
replacements["varsHeadScript"] = base::StringPrintf(
"let abp = %s;", base::ToString(base::FeatureList::IsEnabled(
ntp_features::kNtpOneGoogleBarAsyncBarParts)));
replacements["inHeadScript"] = data->in_head_script;
replacements["inHeadStyle"] = data->in_head_style;
replacements["afterBarScript"] = data->after_bar_script;
replacements["endOfBodyHtml"] = data->end_of_body_html;
replacements["endOfBodyScript"] = data->end_of_body_script;
html = FormatTemplate(IDR_NEW_TAB_PAGE_UNTRUSTED_ONE_GOOGLE_BAR_HTML,
replacements);
}
auto html_ref_counted =
base::MakeRefCounted<base::RefCountedString>(std::move(html));
for (auto& callback : one_google_bar_callbacks_) {
std::move(callback).Run(html_ref_counted);
}
one_google_bar_callbacks_.clear();
}
void UntrustedSource::OnOneGoogleBarServiceShuttingDown() {
one_google_bar_service_observation_.Reset();
one_google_bar_service_ = nullptr;
}
void UntrustedSource::ServeBackgroundImage(
const GURL& url,
const GURL& url_2x,
const std::string& size,
const std::string& repeat_x,
const std::string& repeat_y,
const std::string& position_x,
const std::string& position_y,
const std::string& scrim_display,
content::URLDataSource::GotDataCallback callback) {
if (!IsURLAllowed(url)) {
std::move(callback).Run(base::MakeRefCounted<base::RefCountedString>());
return;
}
ui::TemplateReplacements replacements;
replacements["url"] = url.spec();
if (IsURLAllowed(url_2x)) {
replacements["backgroundUrl"] =
base::StringPrintf("image-set(url(%s) 1x, url(%s) 2x)",
url.spec().c_str(), url_2x.spec().c_str());
} else {
replacements["backgroundUrl"] =
base::StringPrintf("url(%s)", url.spec().c_str());
}
replacements["size"] = size;
replacements["repeatX"] = repeat_x;
replacements["repeatY"] = repeat_y;
replacements["positionX"] = position_x;
replacements["positionY"] = position_y;
replacements["scrimDisplay"] = scrim_display;
std::move(callback).Run(
base::MakeRefCounted<base::RefCountedString>(FormatTemplate(
IDR_NEW_TAB_PAGE_UNTRUSTED_BACKGROUND_IMAGE_HTML, replacements)));
}
bool UntrustedSource::IsURLAllowed(const GURL& url) {
if (!url.is_valid() ||
!(url.SchemeIs(url::kHttpsScheme) ||
url.SchemeIs(content::kChromeUIUntrustedScheme)) ||
IsURLBlockedByPolicy(url)) {
LOG(WARNING) << "URL is not allowed.";
return false;
}
return true;
}
bool UntrustedSource::IsURLBlockedByPolicy(const GURL& url) {
PolicyBlocklistService* service =
ChromePolicyBlocklistServiceFactory::GetForProfile(profile_);
URLBlocklistState blocklist_state = service->GetURLBlocklistState(url);
if (blocklist_state == URLBlocklistState::URL_IN_BLOCKLIST) {
LOG(WARNING) << "URL is blocked by a policy.";
return true;
}
return false;
}