910e62b5创建于 1月15日历史提交
// 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