blob: cddac2fb3466ef1f4597b6c34d1703129b3a84ea [file] [log] [blame]
// Copyright 2023 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/btm/btm_test_utils.h"
#include <string_view>
#include "base/strings/stringprintf.h"
#include "base/test/bind.h"
#include "components/content_settings/core/common/content_settings.h"
#include "components/content_settings/core/common/content_settings_pattern.h"
#include "components/content_settings/core/common/content_settings_utils.h"
#include "content/browser/btm/btm_service_impl.h"
#include "content/browser/renderer_host/navigation_request.h"
#include "content/public/browser/browsing_data_remover.h"
#include "content/public/browser/btm_service.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/hit_test_region_observer.h"
#include "content/public/test/test_frame_navigation_observer.h"
#include "content/public/test/test_utils.h"
#include "net/base/schemeful_site.h"
#include "net/cookies/cookie_setting_override.h"
#include "net/cookies/site_for_cookies.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "services/metrics/public/cpp/ukm_source_id.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "third_party/blink/public/mojom/frame/frame.mojom-shared.h"
namespace content {
void CloseTab(WebContents* web_contents) {
WebContentsDestroyedWatcher destruction_watcher(web_contents);
web_contents->Close();
destruction_watcher.Wait();
}
base::expected<WebContents*, std::string> OpenInNewTab(
WebContents* original_tab,
const GURL& url) {
OpenedWindowObserver tab_observer(original_tab,
WindowOpenDisposition::NEW_FOREGROUND_TAB);
if (!ExecJs(original_tab, JsReplace("window.open($1, '_blank');", url))) {
return base::unexpected("window.open failed");
}
tab_observer.Wait();
// Wait for the new tab to finish navigating.
WaitForLoadStop(tab_observer.window());
return tab_observer.window();
}
[[nodiscard]] testing::AssertionResult AccessStorage(
RenderFrameHost* frame,
blink::mojom::StorageTypeAccessed type) {
// We drop the first character of ToString(type) because it's just the
// constant-indicating 'k'.
return ExecJs(frame,
base::StringPrintf(kStorageAccessScript,
base::ToString(type).substr(1).c_str()),
EXECUTE_SCRIPT_NO_USER_GESTURE,
/*world_id=*/1);
}
void AccessCookieViaJSIn(WebContents* web_contents, RenderFrameHost* frame) {
FrameCookieAccessObserver observer(web_contents, frame,
CookieOperation::kChange);
ASSERT_TRUE(ExecJs(frame, "document.cookie = 'foo=bar';",
EXECUTE_SCRIPT_NO_USER_GESTURE));
observer.Wait();
}
[[nodiscard]] testing::AssertionResult ClientSideRedirectViaMetaTag(
WebContents* web_contents,
RenderFrameHost* frame,
const GURL& target_url,
const std::optional<const GURL>& expected_commit_url) {
TestFrameNavigationObserver nav_observer(frame);
bool js_succeeded = ExecJs(frame,
JsReplace(
R"(
function redirectViaMetaTag() {
var element = document.createElement('meta');
element.setAttribute('http-equiv', 'refresh');
element.setAttribute('content', '0; url=$1');
document.getElementsByTagName('head')[0].appendChild(element);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", redirectViaMetaTag);
} else {
redirectViaMetaTag();
}
)",
target_url),
EXECUTE_SCRIPT_NO_USER_GESTURE);
if (!js_succeeded) {
return testing::AssertionFailure()
<< "Failed to execute script to client-side redirect to URL "
<< target_url;
}
nav_observer.Wait();
if (nav_observer.last_committed_url() ==
expected_commit_url.value_or(target_url)) {
return testing::AssertionSuccess();
} else {
return testing::AssertionFailure()
<< "Expected to arrive at "
<< expected_commit_url.value_or(target_url)
<< " but URL was actually " << nav_observer.last_committed_url();
}
}
[[nodiscard]] testing::AssertionResult ClientSideRedirectViaJS(
WebContents* web_contents,
RenderFrameHost* frame,
const GURL& target_url,
const std::optional<const GURL>& expected_commit_url) {
TestFrameNavigationObserver nav_observer(frame);
bool js_succeeded =
ExecJs(frame, JsReplace(R"(window.location.replace($1);)", target_url),
EXECUTE_SCRIPT_NO_USER_GESTURE);
if (!js_succeeded) {
return testing::AssertionFailure()
<< "Failed to execute script to client-side redirect to URL "
<< target_url;
}
nav_observer.Wait();
if (nav_observer.last_committed_url() ==
expected_commit_url.value_or(target_url)) {
return testing::AssertionSuccess();
} else {
return testing::AssertionFailure()
<< "Expected to arrive at "
<< expected_commit_url.value_or(target_url)
<< " but URL was actually " << nav_observer.last_committed_url();
}
}
std::string StringifyBtmClientRedirectMethod(
BtmClientRedirectMethod client_redirect_method) {
switch (client_redirect_method) {
case BtmClientRedirectMethod::kMetaTag:
return "MetaTag";
case BtmClientRedirectMethod::kJsWindowLocationReplace:
return "JsWindowLocationReplace";
case BtmClientRedirectMethod::kRedirectLikeNavigation:
return "RedirectLikeNavigation";
}
}
[[nodiscard]] testing::AssertionResult PerformClientRedirect(
BtmClientRedirectMethod redirect_method,
WebContents* web_contents,
const GURL& redirect_url,
const std::optional<const GURL>& expected_commit_url) {
const GURL& commit_url = expected_commit_url.value_or(redirect_url);
switch (redirect_method) {
case BtmClientRedirectMethod::kMetaTag:
return ClientSideRedirectViaMetaTag(web_contents,
web_contents->GetPrimaryMainFrame(),
redirect_url, commit_url);
case BtmClientRedirectMethod::kJsWindowLocationReplace:
return ClientSideRedirectViaJS(web_contents,
web_contents->GetPrimaryMainFrame(),
redirect_url, commit_url);
case BtmClientRedirectMethod::kRedirectLikeNavigation:
return testing::AssertionResult(
NavigateToURLFromRendererWithoutUserGesture(
web_contents, redirect_url, commit_url));
}
}
bool NavigateToSetCookie(WebContents* web_contents,
const net::EmbeddedTestServer* server,
std::string_view host,
bool is_secure_cookie_set,
bool is_ad_tagged) {
std::string relative_url = "/set-cookie?name=value";
if (is_secure_cookie_set) {
relative_url += ";Secure;SameSite=None";
}
if (is_ad_tagged) {
relative_url += "&isad=1";
}
const auto url = server->GetURL(host, relative_url);
URLCookieAccessObserver observer(web_contents, url, CookieOperation::kChange);
bool success = NavigateToURL(web_contents, url);
if (success) {
observer.Wait();
}
return success;
}
void CreateImageAndWaitForCookieAccess(WebContents* web_contents,
const GURL& image_url) {
URLCookieAccessObserver observer(web_contents, image_url,
CookieOperation::kRead);
ASSERT_TRUE(ExecJs(web_contents,
JsReplace(
R"(
let img = document.createElement('img');
img.src = $1;
document.body.appendChild(img);)",
image_url),
EXECUTE_SCRIPT_NO_USER_GESTURE));
// The image must cause a cookie access, or else this will hang.
observer.Wait();
}
std::optional<StateValue> GetBtmState(BtmServiceImpl* btm_service,
const GURL& url) {
std::optional<StateValue> state;
auto* storage = btm_service->storage();
DCHECK(storage);
storage->AsyncCall(&BtmStorage::Read)
.WithArgs(url)
.Then(base::BindLambdaForTesting([&](const BtmState& loaded_state) {
if (loaded_state.was_loaded()) {
state = loaded_state.ToStateValue();
}
}));
WaitOnStorage(btm_service);
return state;
}
URLCookieAccessObserver::URLCookieAccessObserver(WebContents* web_contents,
const GURL& url,
CookieOperation access_type)
: WebContentsObserver(web_contents), url_(url), access_type_(access_type) {}
void URLCookieAccessObserver::Wait() {
run_loop_.Run();
}
void URLCookieAccessObserver::OnCookiesAccessed(
RenderFrameHost* render_frame_host,
const CookieAccessDetails& details) {
cookie_accessed_in_primary_page_ = IsInPrimaryPage(*render_frame_host);
if (details.type == access_type_ && details.url == url_) {
run_loop_.Quit();
}
}
void URLCookieAccessObserver::OnCookiesAccessed(
NavigationHandle* navigation_handle,
const CookieAccessDetails& details) {
cookie_accessed_in_primary_page_ = IsInPrimaryPage(*navigation_handle);
if (details.type == access_type_ && details.url == url_) {
run_loop_.Quit();
}
}
bool URLCookieAccessObserver::CookieAccessedInPrimaryPage() const {
return cookie_accessed_in_primary_page_;
}
FrameCookieAccessObserver::FrameCookieAccessObserver(
WebContents* web_contents,
RenderFrameHost* render_frame_host,
CookieOperation access_type)
: WebContentsObserver(web_contents),
render_frame_host_(render_frame_host),
access_type_(access_type) {}
void FrameCookieAccessObserver::Wait() {
run_loop_.Run();
}
void FrameCookieAccessObserver::OnCookiesAccessed(
RenderFrameHost* render_frame_host,
const CookieAccessDetails& details) {
if (details.type == access_type_ && render_frame_host_ == render_frame_host) {
run_loop_.Quit();
}
}
UserActivationObserver::UserActivationObserver(
WebContents* web_contents,
RenderFrameHost* render_frame_host)
: WebContentsObserver(web_contents),
render_frame_host_(render_frame_host) {}
void UserActivationObserver::Wait() {
run_loop_.Run();
}
void UserActivationObserver::FrameReceivedUserActivation(
RenderFrameHost* render_frame_host) {
if (render_frame_host_ == render_frame_host) {
run_loop_.Quit();
}
}
EntryUrlsAre::EntryUrlsAre(std::string entry_name,
std::vector<std::string> urls)
: entry_name_(std::move(entry_name)), expected_urls_(std::move(urls)) {
// Sort the URLs before comparing, so order doesn't matter. (BtmDatabase
// currently sorts its results, but that could change and these tests
// shouldn't care.)
std::sort(expected_urls_.begin(), expected_urls_.end());
}
EntryUrlsAre::EntryUrlsAre(const EntryUrlsAre&) = default;
EntryUrlsAre::EntryUrlsAre(EntryUrlsAre&&) = default;
EntryUrlsAre::~EntryUrlsAre() = default;
bool EntryUrlsAre::MatchAndExplain(
const ukm::TestUkmRecorder& ukm_recorder,
testing::MatchResultListener* result_listener) const {
std::vector<std::string> actual_urls;
for (const ukm::mojom::UkmEntry* entry :
ukm_recorder.GetEntriesByName(entry_name_)) {
GURL url = ukm_recorder.GetSourceForSourceId(entry->source_id)->url();
actual_urls.push_back(url.spec());
}
std::sort(actual_urls.begin(), actual_urls.end());
// ExplainMatchResult() won't print out the full contents of `actual_urls`,
// so for more helpful error messages, we do it ourselves.
*result_listener << "whose entries for '" << entry_name_
<< "' contain the URLs "
<< testing::PrintToString(actual_urls) << ", ";
// Use ContainerEq() instead of e.g. ElementsAreArray() because the error
// messages are much more useful.
return ExplainMatchResult(testing::ContainerEq(expected_urls_), actual_urls,
result_listener);
}
void EntryUrlsAre::DescribeTo(std::ostream* os) const {
*os << "has entries for '" << entry_name_ << "' whose URLs are "
<< testing::PrintToString(expected_urls_);
}
void EntryUrlsAre::DescribeNegationTo(std::ostream* os) const {
*os << "does not have entries for '" << entry_name_ << "' whose URLs are "
<< testing::PrintToString(expected_urls_);
}
ScopedInitFeature::ScopedInitFeature(const base::Feature& feature,
bool enable,
const base::FieldTrialParams& params) {
if (enable) {
feature_list_.InitAndEnableFeatureWithParameters(feature, params);
} else {
feature_list_.InitAndDisableFeature(feature);
}
}
ScopedInitBtmFeature::ScopedInitBtmFeature(bool enable,
const base::FieldTrialParams& params)
: init_feature_(features::kBtm, enable, params) {}
OpenedWindowObserver::OpenedWindowObserver(
WebContents* web_contents,
WindowOpenDisposition open_disposition)
: WebContentsObserver(web_contents), open_disposition_(open_disposition) {}
void OpenedWindowObserver::DidOpenRequestedURL(
WebContents* new_contents,
RenderFrameHost* source_render_frame_host,
const GURL& url,
const Referrer& referrer,
WindowOpenDisposition disposition,
ui::PageTransition transition,
bool started_from_context_menu,
bool renderer_initiated) {
if (!window_ && disposition == open_disposition_) {
window_ = new_contents;
run_loop_.Quit();
}
}
// TODO - crbug.com/40247129: Remove this method in favor of directly using
// SimulateMouseClickAndWait() once mouse clicks / taps reliably trigger user
// activation on Android
void SimulateUserActivation(WebContents* web_contents) {
#if BUILDFLAG(IS_ANDROID)
ASSERT_TRUE(ExecJs(web_contents, ""));
#else
SimulateMouseClickAndWait(web_contents);
#endif
}
void SimulateMouseClickAndWait(WebContents* web_contents) {
WaitForHitTestData(web_contents->GetPrimaryMainFrame());
UserActivationObserver observer(web_contents,
web_contents->GetPrimaryMainFrame());
SimulateMouseClick(web_contents, 0, blink::WebMouseEvent::Button::kLeft);
observer.Wait();
}
TpcBlockingBrowserClient::TpcBlockingBrowserClient() = default;
TpcBlockingBrowserClient::~TpcBlockingBrowserClient() = default;
bool TpcBlockingBrowserClient::IsFullCookieAccessAllowed(
BrowserContext* browser_context,
WebContents* web_contents,
const GURL& url,
const blink::StorageKey& storage_key,
net::CookieSettingOverrides overrides) {
return IsFullCookieAccessAllowed(url, storage_key.ToNetSiteForCookies(),
storage_key.origin(), overrides,
storage_key.ToCookiePartitionKey());
}
void TpcBlockingBrowserClient::GrantCookieAccessDueToHeuristic(
BrowserContext* browser_context,
const net::SchemefulSite& top_frame_site,
const net::SchemefulSite& accessing_site,
base::TimeDelta ttl,
bool ignore_schemes) {
ContentSettingsPattern primary_pattern =
ContentSettingsPattern::FromURLToSchemefulSitePattern(
accessing_site.GetURL());
ContentSettingsPattern secondary_pattern =
ContentSettingsPattern::FromURLToSchemefulSitePattern(
top_frame_site.GetURL());
if (ignore_schemes) {
primary_pattern =
ContentSettingsPattern::ToHostOnlyPattern(primary_pattern);
secondary_pattern =
ContentSettingsPattern::ToHostOnlyPattern(secondary_pattern);
}
tpc_content_settings_.SetValue(primary_pattern, secondary_pattern,
base::Value(CONTENT_SETTING_ALLOW),
/*metadata=*/{});
}
bool TpcBlockingBrowserClient::AreThirdPartyCookiesGenerallyAllowed(
BrowserContext* browser_context,
WebContents* web_contents) {
return !block_3pcs_;
}
bool TpcBlockingBrowserClient::ShouldBtmDeleteInteractionRecords(
uint64_t remove_mask) {
return remove_mask & TpcBlockingBrowserClient::DATA_TYPE_HISTORY;
}
void TpcBlockingBrowserClient::AllowThirdPartyCookiesOnSite(const GURL& url) {
tpc_content_settings_.SetValue(
ContentSettingsPattern::Wildcard(),
ContentSettingsPattern::FromURLToSchemefulSitePattern(url),
base::Value(CONTENT_SETTING_ALLOW), /*metadata=*/{});
}
void TpcBlockingBrowserClient::GrantCookieAccessTo3pSite(const GURL& url) {
tpc_content_settings_.SetValue(
ContentSettingsPattern::FromURLToSchemefulSitePattern(url),
ContentSettingsPattern::Wildcard(), base::Value(CONTENT_SETTING_ALLOW),
/*metadata=*/{});
}
void TpcBlockingBrowserClient::BlockThirdPartyCookiesOnSite(const GURL& url) {
tpc_content_settings_.SetValue(
ContentSettingsPattern::Wildcard(),
ContentSettingsPattern::FromURLToSchemefulSitePattern(url),
base::Value(CONTENT_SETTING_BLOCK), /*metadata=*/{});
}
void TpcBlockingBrowserClient::BlockThirdPartyCookies(
const GURL& url,
const GURL& first_party_url) {
tpc_content_settings_.SetValue(
ContentSettingsPattern::FromURLToSchemefulSitePattern(url),
ContentSettingsPattern::FromURLToSchemefulSitePattern(first_party_url),
base::Value(CONTENT_SETTING_BLOCK), /*metadata=*/{});
}
// Overrides for content_settings::CookieSettingsBase
bool TpcBlockingBrowserClient::ShouldIgnoreSameSiteRestrictions(
const GURL& url,
const net::SiteForCookies& site_for_cookies) const {
return false;
}
ContentSetting TpcBlockingBrowserClient::GetContentSetting(
const GURL& primary_url,
const GURL& secondary_url,
ContentSettingsType content_type,
content_settings::SettingInfo* info) const {
if (content_type != ContentSettingsType::COOKIES) {
return CONTENT_SETTING_DEFAULT;
}
const content_settings::RuleEntry* rule_entry =
tpc_content_settings_.Find(primary_url, secondary_url);
if (rule_entry == nullptr) {
if (info) {
info->primary_pattern = ContentSettingsPattern::Wildcard();
info->secondary_pattern = ContentSettingsPattern::Wildcard();
}
// By default we'll allow cookies. Blocking by default will override any 3PC
// exemption settings.
return CONTENT_SETTING_ALLOW;
}
if (info) {
info->primary_pattern = rule_entry->first.primary_pattern;
info->secondary_pattern = rule_entry->first.secondary_pattern;
}
return content_settings::ValueToContentSetting(rule_entry->second.value);
}
bool TpcBlockingBrowserClient::ShouldAlwaysAllowCookies(
const GURL& url,
const GURL& first_party_url) const {
return false;
}
bool TpcBlockingBrowserClient::ShouldBlockThirdPartyCookies(
base::optional_ref<const url::Origin> top_frame_origin,
net::CookieSettingOverrides overrides) const {
return block_3pcs_;
}
bool TpcBlockingBrowserClient::MitigationsEnabledFor3pcd() const {
return false;
}
bool TpcBlockingBrowserClient::IsThirdPartyCookiesAllowedScheme(
std::string_view scheme) const {
return false;
}
PausedCookieAccessObservers::PausedCookieAccessObservers(
NotifyCookiesAccessedCallback callback,
PendingObserversWithContext observers)
: CookieAccessObservers(std::move(callback)),
pending_receivers_(std::move(observers)) {}
PausedCookieAccessObservers::~PausedCookieAccessObservers() = default;
void PausedCookieAccessObservers::Add(
mojo::PendingReceiver<network::mojom::CookieAccessObserver> receiver,
CookieAccessDetails::Source source) {
pending_receivers_.emplace_back(std::move(receiver), source);
}
PausedCookieAccessObservers::PendingObserversWithContext
PausedCookieAccessObservers::TakeReceiversWithContext() {
return std::exchange(pending_receivers_, {});
}
CookieAccessInterceptor::CookieAccessInterceptor(WebContents& web_contents)
: WebContentsObserver(&web_contents) {}
CookieAccessInterceptor::~CookieAccessInterceptor() = default;
void CookieAccessInterceptor::DidStartNavigation(
NavigationHandle* navigation_handle) {
auto& request = *NavigationRequest::From(navigation_handle);
auto observers = std::make_unique<PausedCookieAccessObservers>(
base::BindRepeating(&NavigationRequest::NotifyCookiesAccessed,
// Unretained is safe here because ownership of the
// observers is passed to the request below.
base::Unretained(&request)),
request.TakeCookieObservers());
request.SetCookieAccessObserversForTesting(std::move(observers));
}
} // namespace content