// Copyright 2019 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/client_hints/client_hints.h"

#include <algorithm>
#include <string>

#include "base/command_line.h"
#include "base/containers/contains.h"
#include "base/feature_list.h"
#include "base/metrics/field_trial_params.h"
#include "base/metrics/histogram_functions.h"
#include "base/numerics/safe_conversions.h"
#include "base/rand_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "content/browser/devtools/devtools_instrumentation.h"
#include "content/browser/preloading/prerender/prerender_host.h"
#include "content/browser/renderer_host/frame_tree.h"
#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/browser/renderer_host/navigator.h"
#include "content/browser/renderer_host/navigator_delegate.h"
#include "content/browser/renderer_host/render_view_host_impl.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/client_hints_controller_delegate.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/host_zoom_map.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_features.h"
#include "content/public/common/content_switches.h"
#include "net/base/url_util.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_response_headers.h"
#include "net/http/structured_headers.h"
#include "net/nqe/effective_connection_type.h"
#include "net/nqe/network_quality_estimator_params.h"
#include "services/network/public/cpp/client_hints.h"
#include "services/network/public/cpp/is_potentially_trustworthy.h"
#include "services/network/public/cpp/network_quality_tracker.h"
#include "services/network/public/mojom/web_client_hints_types.mojom-shared.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "third_party/blink/public/common/client_hints/client_hints.h"
#include "third_party/blink/public/common/client_hints/enabled_client_hints.h"
#include "third_party/blink/public/common/device_memory/approximated_device_memory.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/page/page_zoom.h"
#include "third_party/blink/public/common/permissions_policy/origin_with_possible_wildcards.h"
#include "third_party/blink/public/common/permissions_policy/permissions_policy.h"
#include "third_party/blink/public/common/user_agent/user_agent_metadata.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "url/origin.h"
#include "url/url_constants.h"

namespace content {

namespace {
using ::network::mojom::WebClientHintsType;

uint8_t randomization_salt = 0;

constexpr size_t kMaxRandomNumbers = 21;

// Returns the randomization salt (weak and insecure) that should be used when
// adding noise to the network quality metrics. This is known only to the
// device, and is generated only once. This makes it possible to add the same
// amount of noise for a given origin.
uint8_t RandomizationSalt() {
  if (randomization_salt == 0)
    randomization_salt = base::RandInt(1, kMaxRandomNumbers);
  DCHECK_LE(1, randomization_salt);
  DCHECK_GE(kMaxRandomNumbers, randomization_salt);
  return randomization_salt;
}

double GetRandomMultiplier(const std::string& host) {
  // The random number should be a function of the hostname to reduce
  // cross-origin fingerprinting. The random number should also be a function
  // of randomized salt which is known only to the device. This prevents
  // origin from removing noise from the estimates.
  unsigned hash = std::hash<std::string>{}(host) + RandomizationSalt();
  double random_multiplier =
      0.9 + static_cast<double>((hash % kMaxRandomNumbers)) * 0.01;
  DCHECK_LE(0.90, random_multiplier);
  DCHECK_GE(1.10, random_multiplier);
  return random_multiplier;
}

unsigned long RoundRtt(const std::string& host,
                       const absl::optional<base::TimeDelta>& rtt) {
  if (!rtt.has_value()) {
    // RTT is unavailable. So, return the fastest value.
    return 0;
  }

  // Limit the maximum reported value and the granularity to reduce
  // fingerprinting.
  constexpr base::TimeDelta kMaxRtt = base::Seconds(3);
  constexpr base::TimeDelta kGranularity = base::Milliseconds(50);

  const base::TimeDelta modified_rtt =
      std::min(rtt.value() * GetRandomMultiplier(host), kMaxRtt);
  DCHECK_GE(modified_rtt, base::TimeDelta());
  return modified_rtt.RoundToMultiple(kGranularity).InMilliseconds();
}

double RoundKbpsToMbps(const std::string& host,
                       const absl::optional<int32_t>& downlink_kbps) {
  // Limit the size of the buckets and the maximum reported value to reduce
  // fingerprinting.
  static const size_t kGranularityKbps = 50;
  static const double kMaxDownlinkKbps = 10.0 * 1000;

  // If downlink is unavailable, return the fastest value.
  double randomized_downlink_kbps = downlink_kbps.value_or(kMaxDownlinkKbps);
  randomized_downlink_kbps *= GetRandomMultiplier(host);

  randomized_downlink_kbps =
      std::min(randomized_downlink_kbps, kMaxDownlinkKbps);

  DCHECK_LE(0, randomized_downlink_kbps);
  DCHECK_GE(kMaxDownlinkKbps, randomized_downlink_kbps);
  // Round down to the nearest kGranularityKbps kbps value.
  double downlink_kbps_rounded =
      std::round(randomized_downlink_kbps / kGranularityKbps) *
      kGranularityKbps;

  // Convert from Kbps to Mbps.
  return downlink_kbps_rounded / 1000;
}

double GetDeviceScaleFactor() {
  double device_scale_factor = 1.0;
  if (display::Screen::GetScreen()) {
    device_scale_factor =
        display::Screen::GetScreen()->GetPrimaryDisplay().device_scale_factor();
  }
  DCHECK_LT(0.0, device_scale_factor);
  return device_scale_factor;
}

// Returns the zoom factor for a given |url|.
double GetZoomFactor(BrowserContext* context, const GURL& url) {
#if BUILDFLAG(IS_ANDROID)
  // On Android, use the default value when the AccessibilityPageZoom
  // feature is not enabled.
  if (!base::FeatureList::IsEnabled(features::kAccessibilityPageZoom))
    return 1.0;
#endif

  double zoom_level = HostZoomMap::GetDefaultForBrowserContext(context)
                          ->GetZoomLevelForHostAndScheme(
                              url.scheme(), net::GetHostOrSpecFromURL(url));

  if (zoom_level == 0.0) {
    // Get default zoom level.
    zoom_level = HostZoomMap::GetDefaultForBrowserContext(context)
                     ->GetDefaultZoomLevel();
  }

  return blink::PageZoomLevelToZoomFactor(zoom_level);
}

// Returns a string corresponding to |value|. The returned string satisfies
// ABNF: 1*DIGIT [ "." 1*DIGIT ]
std::string DoubleToSpecCompliantString(double value) {
  DCHECK_LE(0.0, value);
  std::string result = base::NumberToString(value);
  DCHECK(!result.empty());
  if (value >= 1.0)
    return result;

  DCHECK_LE(0.0, value);
  DCHECK_GT(1.0, value);

  // Check if there is at least one character before period.
  if (result.at(0) != '.')
    return result;

  // '.' is the first character in |result|. Prefix one digit before the
  // period to make it spec compliant.
  return "0" + result;
}

// Return the effective connection type value overridden for web APIs.
// If no override value has been set, a null value is returned.
absl::optional<net::EffectiveConnectionType>
GetWebHoldbackEffectiveConnectionType() {
  if (!base::FeatureList::IsEnabled(
          features::kNetworkQualityEstimatorWebHoldback)) {
    return absl::nullopt;
  }
  std::string effective_connection_type_param =
      base::GetFieldTrialParamValueByFeature(
          features::kNetworkQualityEstimatorWebHoldback,
          "web_effective_connection_type_override");

  absl::optional<net::EffectiveConnectionType> effective_connection_type =
      net::GetEffectiveConnectionTypeForName(effective_connection_type_param);
  DCHECK(effective_connection_type_param.empty() || effective_connection_type);

  if (!effective_connection_type)
    return absl::nullopt;
  DCHECK_NE(net::EFFECTIVE_CONNECTION_TYPE_UNKNOWN,
            effective_connection_type.value());
  return effective_connection_type;
}

void SetHeaderToDouble(net::HttpRequestHeaders* headers,
                       WebClientHintsType client_hint_type,
                       double value) {
  headers->SetHeader(network::GetClientHintToNameMap().at(client_hint_type),
                     DoubleToSpecCompliantString(value));
}

void SetHeaderToInt(net::HttpRequestHeaders* headers,
                    WebClientHintsType client_hint_type,
                    double value) {
  headers->SetHeader(network::GetClientHintToNameMap().at(client_hint_type),
                     base::NumberToString(std::round(value)));
}

void SetHeaderToString(net::HttpRequestHeaders* headers,
                       WebClientHintsType client_hint_type,
                       const std::string& value) {
  headers->SetHeader(network::GetClientHintToNameMap().at(client_hint_type),
                     value);
}

void RemoveClientHintHeader(WebClientHintsType client_hint_type,
                            net::HttpRequestHeaders* headers) {
  headers->RemoveHeader(network::GetClientHintToNameMap().at(client_hint_type));
}

void AddDeviceMemoryHeader(net::HttpRequestHeaders* headers,
                           bool use_deprecated_version = false) {
  DCHECK(headers);
  blink::ApproximatedDeviceMemory::Initialize();
  const float device_memory =
      blink::ApproximatedDeviceMemory::GetApproximatedDeviceMemory();
  DCHECK_LT(0.0, device_memory);
  SetHeaderToDouble(headers,
                    use_deprecated_version
                        ? WebClientHintsType::kDeviceMemory_DEPRECATED
                        : WebClientHintsType::kDeviceMemory,
                    device_memory);
}

void AddDPRHeader(net::HttpRequestHeaders* headers,
                  BrowserContext* context,
                  const GURL& url,
                  bool use_deprecated_version = false) {
  DCHECK(headers);
  DCHECK(context);
  double device_scale_factor = GetDeviceScaleFactor();
  double zoom_factor = GetZoomFactor(context, url);
  SetHeaderToDouble(headers,
                    use_deprecated_version ? WebClientHintsType::kDpr_DEPRECATED
                                           : WebClientHintsType::kDpr,
                    device_scale_factor * zoom_factor);
}

void AddSaveDataHeader(net::HttpRequestHeaders* headers,
                       BrowserContext* context) {
  DCHECK(headers);
  DCHECK(context);
  // Unlike other client hints, this one is only sent when it has a value.
  if (GetContentClient()->browser()->IsDataSaverEnabled(context))
    SetHeaderToString(headers, WebClientHintsType::kSaveData, "on");
}

RenderWidgetHostView* GetRenderWidgetHostViewFromFrameTreeNode(
    FrameTreeNode* frame_tree_node) {
  if (!frame_tree_node || !frame_tree_node->current_frame_host())
    return nullptr;

  return frame_tree_node->current_frame_host()->GetView();
}

gfx::Size GetViewportSize(FrameTreeNode* frame_tree_node,
                          ClientHintsControllerDelegate* delegate) {
  // If possible, return the current viewport size.
  RenderWidgetHostView* view =
      GetRenderWidgetHostViewFromFrameTreeNode(frame_tree_node);
  if (view) {
    return view->GetVisibleViewportSize();
  }

  // Otherwise, use the cached viewport size if it is valid (both dimensions are
  // greater than zero).
  gfx::Size cached_viewport_size =
      delegate->GetMostRecentMainFrameViewportSize();
  if (cached_viewport_size.width() > 0 && cached_viewport_size.height() > 0) {
    return cached_viewport_size;
  }

  // Finally, use the display size if neither of the above methods work. Applies
  // the device scale factor in this case, which is implicitly applied to other
  // viewport sizes already.
  return ScaleToRoundedSize(
      display::Screen::GetScreen()->GetPrimaryDisplay().GetSizeInPixel(),
      1.0 / GetDeviceScaleFactor());
}

gfx::Size GetScaledViewportSize(BrowserContext* context,
                                const GURL& url,
                                FrameTreeNode* frame_tree_node,
                                ClientHintsControllerDelegate* delegate) {
  gfx::Size viewport_size = GetViewportSize(frame_tree_node, delegate);

#if BUILDFLAG(IS_ANDROID)
  // On Android, the viewport is scaled so the width is 980. See
  // https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/css/viewportAndroid.css.
  // TODO(1246208): Improve the usefulness of the viewport client hints for
  // navigation requests.
  if (viewport_size.width() > 0) {
    viewport_size =
        ScaleToRoundedSize(viewport_size, 980.0 / viewport_size.width());
  }

  // On Android, use the default value when the AccessibilityPageZoom
  // feature is not enabled.
  if (!base::FeatureList::IsEnabled(features::kAccessibilityPageZoom)) {
    return viewport_size;
  }
#endif

  base::UmaHistogramBoolean("ClientHints.Viewport.IsDeviceScaleFactorOne",
                            GetDeviceScaleFactor() == 1.0);

  double zoom_factor = GetZoomFactor(context, url);
  if (zoom_factor > 0) {
    viewport_size = ScaleToRoundedSize(viewport_size, 1.0 / zoom_factor);
  }
  return viewport_size;
}

void AddViewportWidthHeader(net::HttpRequestHeaders* headers,
                            BrowserContext* context,
                            const GURL& url,
                            FrameTreeNode* frame_tree_node,
                            ClientHintsControllerDelegate* delegate,
                            bool use_deprecated_version = false) {
  DCHECK(headers);
  DCHECK(context);

  gfx::Size viewport_size =
      GetScaledViewportSize(context, url, frame_tree_node, delegate);

  DCHECK_LT(0, viewport_size.width());
  // TODO(yoav): Find out why this 0 check is needed...
  if (viewport_size.width() > 0) {
    SetHeaderToInt(headers,
                   use_deprecated_version
                       ? WebClientHintsType::kViewportWidth_DEPRECATED
                       : WebClientHintsType::kViewportWidth,
                   viewport_size.width());
  }
}

void AddViewportHeightHeader(net::HttpRequestHeaders* headers,
                             BrowserContext* context,
                             const GURL& url,
                             FrameTreeNode* frame_tree_node,
                             ClientHintsControllerDelegate* delegate) {
  DCHECK(headers);
  DCHECK(context);

  gfx::Size viewport_size =
      GetScaledViewportSize(context, url, frame_tree_node, delegate);

  DCHECK_LT(0, viewport_size.height());
  SetHeaderToInt(headers, network::mojom::WebClientHintsType::kViewportHeight,
                 viewport_size.height());
}

void AddRttHeader(net::HttpRequestHeaders* headers,
                  network::NetworkQualityTracker* network_quality_tracker,
                  const GURL& url) {
  DCHECK(headers);

  absl::optional<net::EffectiveConnectionType> web_holdback_ect =
      GetWebHoldbackEffectiveConnectionType();

  base::TimeDelta http_rtt;
  if (web_holdback_ect.has_value()) {
    http_rtt = net::NetworkQualityEstimatorParams::GetDefaultTypicalHttpRtt(
        web_holdback_ect.value());
  } else if (network_quality_tracker) {
    http_rtt = network_quality_tracker->GetHttpRTT();
  } else {
    http_rtt = net::NetworkQualityEstimatorParams::GetDefaultTypicalHttpRtt(
        net::EFFECTIVE_CONNECTION_TYPE_UNKNOWN);
  }
  SetHeaderToInt(headers, WebClientHintsType::kRtt_DEPRECATED,
                 RoundRtt(url.host(), http_rtt));
}

void AddDownlinkHeader(net::HttpRequestHeaders* headers,
                       network::NetworkQualityTracker* network_quality_tracker,
                       const GURL& url) {
  DCHECK(headers);
  absl::optional<net::EffectiveConnectionType> web_holdback_ect =
      GetWebHoldbackEffectiveConnectionType();

  int32_t downlink_throughput_kbps;

  if (web_holdback_ect.has_value()) {
    downlink_throughput_kbps =
        net::NetworkQualityEstimatorParams::GetDefaultTypicalDownlinkKbps(
            web_holdback_ect.value());
  } else if (network_quality_tracker) {
    downlink_throughput_kbps =
        network_quality_tracker->GetDownstreamThroughputKbps();
  } else {
    downlink_throughput_kbps =
        net::NetworkQualityEstimatorParams::GetDefaultTypicalDownlinkKbps(
            net::EFFECTIVE_CONNECTION_TYPE_UNKNOWN);
  }

  SetHeaderToDouble(headers, WebClientHintsType::kDownlink_DEPRECATED,
                    RoundKbpsToMbps(url.host(), downlink_throughput_kbps));
}

void AddEctHeader(net::HttpRequestHeaders* headers,
                  network::NetworkQualityTracker* network_quality_tracker,
                  const GURL& url) {
  DCHECK(headers);
  DCHECK_EQ(network::kWebEffectiveConnectionTypeMappingCount,
            net::EFFECTIVE_CONNECTION_TYPE_4G + 1u);
  DCHECK_EQ(network::kWebEffectiveConnectionTypeMappingCount,
            static_cast<size_t>(net::EFFECTIVE_CONNECTION_TYPE_LAST));

  absl::optional<net::EffectiveConnectionType> web_holdback_ect =
      GetWebHoldbackEffectiveConnectionType();

  int effective_connection_type;
  if (web_holdback_ect.has_value()) {
    effective_connection_type = web_holdback_ect.value();
  } else if (network_quality_tracker) {
    effective_connection_type =
        static_cast<int>(network_quality_tracker->GetEffectiveConnectionType());
  } else {
    effective_connection_type =
        static_cast<int>(net::EFFECTIVE_CONNECTION_TYPE_UNKNOWN);
  }

  SetHeaderToString(
      headers, WebClientHintsType::kEct_DEPRECATED,
      network::kWebEffectiveConnectionTypeMapping[effective_connection_type]);
}

void AddPrefersColorSchemeHeader(net::HttpRequestHeaders* headers,
                                 FrameTreeNode* frame_tree_node) {
  if (!frame_tree_node)
    return;
  blink::mojom::PreferredColorScheme preferred_color_scheme =
      WebContents::FromRenderFrameHost(frame_tree_node->current_frame_host())
          ->GetOrCreateWebPreferences()
          .preferred_color_scheme;
  bool is_dark_mode =
      preferred_color_scheme == blink::mojom::PreferredColorScheme::kDark;
  SetHeaderToString(headers, WebClientHintsType::kPrefersColorScheme,
                    is_dark_mode ? network::kPrefersColorSchemeDark
                                 : network::kPrefersColorSchemeLight);
}

void AddPrefersReducedMotionHeader(net::HttpRequestHeaders* headers,
                                   FrameTreeNode* frame_tree_node) {
  if (!frame_tree_node)
    return;
  bool prefers_reduced_motion =
      WebContents::FromRenderFrameHost(frame_tree_node->current_frame_host())
          ->GetOrCreateWebPreferences()
          .prefers_reduced_motion;
  SetHeaderToString(headers, WebClientHintsType::kPrefersReducedMotion,
                    prefers_reduced_motion
                        ? network::kPrefersReducedMotionReduce
                        : network::kPrefersReducedMotionNoPreference);
}

bool IsValidURLForClientHints(const url::Origin& origin) {
  return network::IsOriginPotentiallyTrustworthy(origin);
}

bool UserAgentClientHintEnabled() {
  return base::FeatureList::IsEnabled(blink::features::kUserAgentClientHint);
}

void AddUAHeader(net::HttpRequestHeaders* headers,
                 WebClientHintsType type,
                 const std::string& value) {
  SetHeaderToString(headers, type, value);
}

// Creates a serialized string header value out of the input type, using
// structured headers as described in
// https://www.rfc-editor.org/rfc/rfc8941.html.
template <typename T>
const std::string SerializeHeaderString(const T& value) {
  return net::structured_headers::SerializeItem(
             net::structured_headers::Item(value))
      .value_or(std::string());
}

// Returns true iff the `url` is embedded inside a frame that has the
// corresponding Sec-CH-UA-Reduced or Sec-CH-UA-Full client hint and thus, is
// enrolled in the UserAgentReduction or SendFullUserAgentAfterReduction
// Origin Trial.
//
// TODO(crbug.com/1258063): Remove when the UserAgentReduction and
// SendFullUserAgentAfterReduction Origin Trial is finished.
bool IsOriginTrialHintEnabledForFrame(const url::Origin& origin,
                                      const url::Origin& main_frame_origin,
                                      FrameTreeNode* frame_tree_node,
                                      ClientHintsControllerDelegate* delegate,
                                      WebClientHintsType hint_type) {
  // TODO(crbug.com/1300194): Refactor away from the use of FrameTreeNode
  RenderFrameHostImpl* current = frame_tree_node->current_frame_host();
  while (current) {
    const url::Origin& current_origin =
        (current == frame_tree_node->current_frame_host())
            ? origin
            : current->GetLastCommittedOrigin();

    // Don't use Sec-CH-UA-Reduced or Sec-CH-UA-Full from third-party origins if
    // third-party cookies are blocked, so that we don't reveal any more user
    // data than is allowed by the cookie settings.
    if (main_frame_origin.IsSameOriginWith(current_origin) ||
        !delegate->AreThirdPartyCookiesBlocked(current_origin.GetURL(),
                                               current)) {
      blink::EnabledClientHints current_url_hints;
      delegate->GetAllowedClientHintsFromSource(current_origin,
                                                &current_url_hints);
      if (base::Contains(current_url_hints.GetEnabledHints(), hint_type))
        return true;
    }

    current = current->GetParent();
  }
  return false;
}

// TODO(crbug.com/1258063): Delete this function when the UserAgentReduction and
// SendFullUserAgentAfterReduction Origin Trial is finished.
void RemoveAllClientHintsExceptOriginTrialHints(
    const url::Origin& origin,
    FrameTreeNode* frame_tree_node,
    ClientHintsControllerDelegate* delegate,
    std::vector<WebClientHintsType>* accept_ch,
    url::Origin* outermost_main_frame_origin,
    absl::optional<url::Origin>* third_party_origin) {
  RenderFrameHostImpl* main_frame =
      frame_tree_node->frame_tree().GetMainFrame()->GetOutermostMainFrame();

  for (auto it = accept_ch->begin(); it != accept_ch->end();) {
    if (*it == WebClientHintsType::kUAReduced ||
        *it == WebClientHintsType::kFullUserAgent) {
      ++it;
    } else {
      it = accept_ch->erase(it);
    }
  }

  if (!main_frame->GetLastCommittedOrigin().IsSameOriginWith(origin)) {
    // If third-party cookeis are blocked, we will not persist the
    // Sec-CH-UA-Reduced client hint in a third-party context.
    if (delegate->AreThirdPartyCookiesBlocked(
            origin.GetURL(), frame_tree_node->current_frame_host())) {
      accept_ch->clear();
      return;
    }
    // Third-party contexts need the correct main frame URL and third-party
    // URL in order to validate the Origin Trial token correctly, if present.
    *outermost_main_frame_origin = main_frame->GetLastCommittedOrigin();
    *third_party_origin = absl::make_optional(origin);
  }
}

// Captures the state used in applying client hints.
struct ClientHintsExtendedData {
  ClientHintsExtendedData(const url::Origin& origin,
                          FrameTreeNode* frame_tree_node,
                          ClientHintsControllerDelegate* delegate,
                          const absl::optional<GURL>& maybe_request_url)
      : resource_origin(origin) {
    // If the current frame is the outermost main frame, the URL wasn't
    // committed yet, so in order to get the main frame URL, we should use the
    // provided URL instead. Otherwise, the current frame is a subframe and the
    // outermost main frame URL was committed, so we can safely get it from it.
    // Similarly, an in-navigation outermost main frame doesn't yet have a
    // permissions policy.
    is_outermost_main_frame =
        !frame_tree_node || frame_tree_node->IsOutermostMainFrame();
    if (is_outermost_main_frame) {
      main_frame_origin = resource_origin;
    } else if (frame_tree_node->IsInFencedFrameTree()) {
      // TODO(https://crbug.com/1430508) Add WPT tests and specify the behavior
      // of client hints delegation for subframes inside
      // FencedFrames/Portals/etc...
      permissions_policy = blink::PermissionsPolicy::CreateForFencedFrame(
          resource_origin,
          /*is_opaque_ads_mode=*/frame_tree_node
                  ->GetDeprecatedFencedFrameMode() ==
              blink::FencedFrame::DeprecatedFencedFrameMode::kOpaqueAds);
    } else {
      RenderFrameHostImpl* main_frame =
          frame_tree_node->frame_tree().GetMainFrame();
      main_frame_origin = main_frame->GetLastCommittedOrigin();
      permissions_policy = blink::PermissionsPolicy::CopyStateFrom(
          main_frame->permissions_policy());
    }

    const base::TimeTicks start_time = base::TimeTicks::Now();
    delegate->GetAllowedClientHintsFromSource(main_frame_origin, &hints);
    const base::TimeTicks pref_read_time = base::TimeTicks::Now();

    // If this is a prerender tree, also capture prerender local setting. The
    // setting was given by navigation requests on the prerendering page, and
    // has not been used as a global setting.
    if (frame_tree_node && frame_tree_node->frame_tree().is_prerendering()) {
      // If prerender host is nullptr, it means prerender has been canceled and
      // the host will be discarded soon, so we do not need to continue.
      if (auto* host = PrerenderHost::GetPrerenderHostFromFrameTreeNode(
              *frame_tree_node))
        host->GetAllowedClientHintsOnPage(main_frame_origin, &hints);
    }
    const base::TimeTicks prerender_host_time = base::TimeTicks::Now();

    // If this is not a top-level frame, then check if any of the ancestors
    // in the path that led to this request have Sec-CH-UA-Reduced set.
    // TODO(crbug.com/1258063): Remove once the UserAgentReduction Origin Trial
    // is finished.
    if (frame_tree_node && !is_outermost_main_frame) {
      url::Origin trial_origin =
          maybe_request_url ? url::Origin::Create(maybe_request_url.value())
                            : origin;

      is_embedder_ua_reduced = IsOriginTrialHintEnabledForFrame(
          trial_origin, main_frame_origin, frame_tree_node, delegate,
          WebClientHintsType::kUAReduced);
      is_embedder_ua_full = IsOriginTrialHintEnabledForFrame(
          trial_origin, main_frame_origin, frame_tree_node, delegate,
          WebClientHintsType::kFullUserAgent);
    }

    // Record the time spent getting the client hints.
    const base::TimeTicks end_time = base::TimeTicks::Now();
    base::UmaHistogramMicrosecondsTimes("ClientHints.FetchLatency_PrefRead",
                                        pref_read_time - start_time);
    base::UmaHistogramMicrosecondsTimes(
        "ClientHints.FetchLatency_PrerenderHost",
        prerender_host_time - pref_read_time);
    base::UmaHistogramMicrosecondsTimes(
        "ClientHints.FetchLatency_OriginTrialCheck",
        end_time - prerender_host_time);
    base::UmaHistogramMicrosecondsTimes("ClientHints.FetchLatency_Total",
                                        end_time - start_time);
  }

  blink::EnabledClientHints hints;
  // If true, one of the ancestor requests in the path to this request had
  // Sec-CH-UA-Reduced in their Accept-CH cache.  Only applies to embedded
  // requests (top-level requests will always set this to false).
  //
  // If an embedder of a request has Sec-CH-UA-Reduced, it means it will
  // receive the reduced User-Agent header, so we want to also send the reduced
  // User-Agent for the embedded request as well.
  bool is_embedder_ua_reduced = false;
  // If true, one of the ancestor requests in the path to this request had
  // Sec-CH-UA-Full in their Accept-CH cache.  Only applies to embedded
  // requests (top-level requests will always set this to false).
  //
  // If an embedder of a request has Sec-CH-UA-Full, it means it will
  // receive the full User-Agent header, so we want to also send the full
  // User-Agent for the embedded request as well.
  bool is_embedder_ua_full = false;
  url::Origin resource_origin;
  bool is_outermost_main_frame = false;
  url::Origin main_frame_origin;
  std::unique_ptr<blink::PermissionsPolicy> permissions_policy;
};

bool SkipPermissionPolicyCheck(WebClientHintsType type) {
  return type == WebClientHintsType::kUAReduced ||
         type == WebClientHintsType::kFullUserAgent;
}

bool IsClientHintEnabled(const ClientHintsExtendedData& data,
                         WebClientHintsType type) {
  return blink::IsClientHintSentByDefault(type) || data.hints.IsEnabled(type) ||
         (type == WebClientHintsType::kUAReduced &&
          data.is_embedder_ua_reduced) ||
         (type == WebClientHintsType::kFullUserAgent &&
          data.is_embedder_ua_full);
}

bool IsClientHintAllowed(const ClientHintsExtendedData& data,
                         WebClientHintsType type) {
  if (data.is_outermost_main_frame) {
    return true;
  }
  return SkipPermissionPolicyCheck(type) ||
         (data.permissions_policy->IsFeatureEnabledForOrigin(
             blink::GetClientHintToPolicyFeatureMap().at(type),
             data.resource_origin));
}

bool ShouldAddClientHint(const ClientHintsExtendedData& data,
                         WebClientHintsType type) {
  return IsClientHintEnabled(data, type) && IsClientHintAllowed(data, type);
}

bool IsJavascriptEnabled(FrameTreeNode* frame_tree_node) {
  return WebContents::FromRenderFrameHost(frame_tree_node->current_frame_host())
      ->GetOrCreateWebPreferences()
      .javascript_enabled;
}

// This modifies `data.permissions_policy` to reflect any changes to client hint
// permissions which may have occurred via the named accept-ch meta tag.
// The permissions policy the browser side has for the frame was set in stone
// before HTML parsing began, so any updates must be sent via
// `container_policy`.
// TODO(crbug.com/1278127): Replace w/ generic HTML policy modification.
void UpdateIFramePermissionsPolicyWithDelegationSupportForClientHints(
    ClientHintsExtendedData& data,
    const blink::ParsedPermissionsPolicy& container_policy) {
  if (container_policy.empty() ||
      !base::FeatureList::IsEnabled(
          blink::features::kClientHintThirdPartyDelegation)) {
    return;
  }

  // For client hints specifically, we need to allow the container policy
  // to overwrite the parent policy so that permissions policies set in HTML
  // via an accept-ch meta tag can be respected.
  blink::ParsedPermissionsPolicy client_hints_container_policy;
  for (const auto& container_policy_item : container_policy) {
    const auto& it = blink::GetPolicyFeatureToClientHintMap().find(
        container_policy_item.feature);
    if (it != blink::GetPolicyFeatureToClientHintMap().end()) {
      client_hints_container_policy.push_back(container_policy_item);

      // We need to ensure `blink::EnabledClientHints` is updated where the
      // main frame now has permission for the given client hints.
      for (const auto& origin_with_possible_wildcards :
           container_policy_item.allowed_origins) {
        if (origin_with_possible_wildcards.DoesMatchOrigin(
                data.main_frame_origin)) {
          for (const auto& hint : it->second) {
            data.hints.SetIsEnabled(hint, /*should_send*/ true);
          }
          break;
        }
      }
    }
  }
  data.permissions_policy->OverwriteHeaderPolicyForClientHints(
      client_hints_container_policy);
}

// Captures when UpdateNavigationRequestClientUaHeadersImpl() is being called.
enum class ClientUaHeaderCallType {
  // The call is happening during creation of the NavigationRequest.
  kDuringCreation,

  // The call is happening after creation of the NavigationRequest.
  kAfterCreated,
};

// Implementation of UpdateNavigationRequestClientUaHeaders().
void UpdateNavigationRequestClientUaHeadersImpl(
    ClientHintsControllerDelegate* delegate,
    bool override_ua,
    FrameTreeNode* frame_tree_node,
    ClientUaHeaderCallType call_type,
    net::HttpRequestHeaders* headers,
    const blink::ParsedPermissionsPolicy& container_policy,
    const absl::optional<GURL>& request_url,
    const ClientHintsExtendedData& data) {
  absl::optional<blink::UserAgentMetadata> ua_metadata;
  bool disable_due_to_custom_ua = false;
  if (override_ua) {
    NavigatorDelegate* nav_delegate =
        frame_tree_node ? frame_tree_node->navigator().GetDelegate() : nullptr;
    ua_metadata =
        nav_delegate ? nav_delegate->GetUserAgentOverride().ua_metadata_override
                     : absl::nullopt;
    // If a custom UA override is set, but no value is provided for UA client
    // hints, disable them.
    disable_due_to_custom_ua = !ua_metadata.has_value();
  }

  if (frame_tree_node &&
      devtools_instrumentation::ApplyUserAgentMetadataOverrides(frame_tree_node,
                                                                &ua_metadata)) {
    // Likewise, if devtools says to override client hints but provides no
    // value, disable them. This overwrites previous decision from UI.
    disable_due_to_custom_ua = !ua_metadata.has_value();
  }

  if (!disable_due_to_custom_ua) {
    if (!ua_metadata.has_value())
      ua_metadata = delegate->GetUserAgentMetadata();

    // The `Sec-CH-UA` client hint is attached to all outgoing requests. This is
    // (intentionally) different than other client hints.
    // It's barred behind ShouldAddClientHints to make sure it's controlled by
    // Permissions Policy.
    //
    // https://wicg.github.io/client-hints-infrastructure/#abstract-opdef-append-client-hints-to-request
    if (ShouldAddClientHint(data, WebClientHintsType::kUA)) {
      AddUAHeader(headers, WebClientHintsType::kUA,
                  ua_metadata->SerializeBrandMajorVersionList());
    }
    // The `Sec-CH-UA-Mobile client hint was also deemed "low entropy" and can
    // safely be sent with every request. Similarly to UA, ShouldAddClientHints
    // makes sure it's controlled by Permissions Policy.
    if (ShouldAddClientHint(data, WebClientHintsType::kUAMobile)) {
      AddUAHeader(headers, WebClientHintsType::kUAMobile,
                  SerializeHeaderString(ua_metadata->mobile));
    }

    if (ShouldAddClientHint(data, WebClientHintsType::kUAFullVersion)) {
      AddUAHeader(headers, WebClientHintsType::kUAFullVersion,
                  SerializeHeaderString(ua_metadata->full_version));
    }

    if (ShouldAddClientHint(data, WebClientHintsType::kUAArch)) {
      AddUAHeader(headers, WebClientHintsType::kUAArch,
                  SerializeHeaderString(ua_metadata->architecture));
    }

    if (ShouldAddClientHint(data, WebClientHintsType::kUAPlatform)) {
      AddUAHeader(headers, WebClientHintsType::kUAPlatform,
                  SerializeHeaderString(ua_metadata->platform));
    }

    if (ShouldAddClientHint(data, WebClientHintsType::kUAPlatformVersion)) {
      AddUAHeader(headers, WebClientHintsType::kUAPlatformVersion,
                  SerializeHeaderString(ua_metadata->platform_version));
    }

    if (ShouldAddClientHint(data, WebClientHintsType::kUAModel)) {
      AddUAHeader(headers, WebClientHintsType::kUAModel,
                  SerializeHeaderString(ua_metadata->model));
    }
    if (ShouldAddClientHint(data, WebClientHintsType::kUABitness)) {
      AddUAHeader(headers, WebClientHintsType::kUABitness,
                  SerializeHeaderString(ua_metadata->bitness));
    }
    if (ShouldAddClientHint(data, WebClientHintsType::kUAWoW64)) {
      AddUAHeader(headers, WebClientHintsType::kUAWoW64,
                  SerializeHeaderString(ua_metadata->wow64));
    }
    if (ShouldAddClientHint(data, WebClientHintsType::kUAReduced)) {
      AddUAHeader(headers, WebClientHintsType::kUAReduced,
                  SerializeHeaderString(true));
    }
    if (ShouldAddClientHint(data, WebClientHintsType::kUAFullVersionList)) {
      AddUAHeader(headers, WebClientHintsType::kUAFullVersionList,
                  ua_metadata->SerializeBrandFullVersionList());
    }
    if (ShouldAddClientHint(data, WebClientHintsType::kFullUserAgent)) {
      AddUAHeader(headers, WebClientHintsType::kFullUserAgent,
                  SerializeHeaderString(true));
    }
  } else if (call_type == ClientUaHeaderCallType::kAfterCreated) {
    RemoveClientHintHeader(WebClientHintsType::kUA, headers);
    RemoveClientHintHeader(WebClientHintsType::kUAMobile, headers);
    RemoveClientHintHeader(WebClientHintsType::kUAFullVersion, headers);
    RemoveClientHintHeader(WebClientHintsType::kUAArch, headers);
    RemoveClientHintHeader(WebClientHintsType::kUAPlatform, headers);
    RemoveClientHintHeader(WebClientHintsType::kUAPlatformVersion, headers);
    RemoveClientHintHeader(WebClientHintsType::kUAModel, headers);
    RemoveClientHintHeader(WebClientHintsType::kUABitness, headers);
    RemoveClientHintHeader(WebClientHintsType::kUAReduced, headers);
    RemoveClientHintHeader(WebClientHintsType::kUAFullVersionList, headers);
    RemoveClientHintHeader(WebClientHintsType::kUAWoW64, headers);
    RemoveClientHintHeader(WebClientHintsType::kFullUserAgent, headers);
  }
}

}  // namespace

bool ShouldAddClientHints(const url::Origin& origin,
                          FrameTreeNode* frame_tree_node,
                          ClientHintsControllerDelegate* delegate,
                          const absl::optional<GURL> maybe_request_url) {
  url::Origin origin_to_check =
      maybe_request_url ? url::Origin::Create(maybe_request_url.value())
                        : origin;
  // Client hints should only be enabled when JavaScript is enabled. Platforms
  // which enable/disable JavaScript on a per-origin basis should implement
  // IsJavaScriptAllowed to check a given origin. Other platforms (Android
  // WebView) enable/disable JavaScript on a per-View basis, using the
  // WebPreferences setting.
  return IsValidURLForClientHints(origin_to_check) &&
         delegate->IsJavaScriptAllowed(
             origin_to_check.GetURL(),
             frame_tree_node ? frame_tree_node->GetParentOrOuterDocument()
                             : nullptr) &&
         (!frame_tree_node || IsJavascriptEnabled(frame_tree_node));
}

unsigned long RoundRttForTesting(const std::string& host,
                                 const absl::optional<base::TimeDelta>& rtt) {
  return RoundRtt(host, rtt);
}

double RoundKbpsToMbpsForTesting(const std::string& host,
                                 const absl::optional<int32_t>& downlink_kbps) {
  return RoundKbpsToMbps(host, downlink_kbps);
}

void UpdateNavigationRequestClientUaHeaders(
    const url::Origin& origin,
    ClientHintsControllerDelegate* delegate,
    bool override_ua,
    FrameTreeNode* frame_tree_node,
    net::HttpRequestHeaders* headers,
    const absl::optional<GURL>& request_url) {
  DCHECK(frame_tree_node);
  if (!UserAgentClientHintEnabled() ||
      !ShouldAddClientHints(origin, frame_tree_node, delegate, request_url)) {
    return;
  }

  ClientHintsExtendedData data(origin, frame_tree_node, delegate, request_url);
  UpdateNavigationRequestClientUaHeadersImpl(
      delegate, override_ua, frame_tree_node,
      ClientUaHeaderCallType::kAfterCreated, headers, {}, request_url, data);
}

namespace {

void AddRequestClientHintsHeaders(
    const url::Origin& origin,
    net::HttpRequestHeaders* headers,
    BrowserContext* context,
    ClientHintsControllerDelegate* delegate,
    bool is_ua_override_on,
    FrameTreeNode* frame_tree_node,
    const blink::ParsedPermissionsPolicy& container_policy,
    const absl::optional<GURL>& request_url) {
  ClientHintsExtendedData data(origin, frame_tree_node, delegate, request_url);
  UpdateIFramePermissionsPolicyWithDelegationSupportForClientHints(
      data, container_policy);

  GURL url = origin.GetURL();

  // Add Headers
  if (ShouldAddClientHint(data, WebClientHintsType::kDeviceMemory_DEPRECATED)) {
    AddDeviceMemoryHeader(headers, /*use_deprecated_version*/ true);
  }
  if (ShouldAddClientHint(data, WebClientHintsType::kDeviceMemory)) {
    AddDeviceMemoryHeader(headers);
  }
  if (ShouldAddClientHint(data, WebClientHintsType::kDpr_DEPRECATED)) {
    AddDPRHeader(headers, context, url, /*use_deprecated_version*/ true);
  }
  if (ShouldAddClientHint(data, WebClientHintsType::kDpr)) {
    AddDPRHeader(headers, context, url);
  }
  if (ShouldAddClientHint(data,
                          WebClientHintsType::kViewportWidth_DEPRECATED)) {
    AddViewportWidthHeader(headers, context, url, frame_tree_node, delegate,
                           /*use_deprecated_version*/ true);
  }
  if (ShouldAddClientHint(data, WebClientHintsType::kViewportWidth)) {
    AddViewportWidthHeader(headers, context, url, frame_tree_node, delegate);
  }
  if (ShouldAddClientHint(
          data, network::mojom::WebClientHintsType::kViewportHeight)) {
    AddViewportHeightHeader(headers, context, url, frame_tree_node, delegate);
  }
  network::NetworkQualityTracker* network_quality_tracker =
      delegate->GetNetworkQualityTracker();
  if (ShouldAddClientHint(data, WebClientHintsType::kRtt_DEPRECATED)) {
    AddRttHeader(headers, network_quality_tracker, url);
  }
  if (ShouldAddClientHint(data, WebClientHintsType::kDownlink_DEPRECATED)) {
    AddDownlinkHeader(headers, network_quality_tracker, url);
  }
  if (ShouldAddClientHint(data, WebClientHintsType::kEct_DEPRECATED)) {
    AddEctHeader(headers, network_quality_tracker, url);
  }

  if (UserAgentClientHintEnabled()) {
    UpdateNavigationRequestClientUaHeadersImpl(
        delegate, is_ua_override_on, frame_tree_node,
        ClientUaHeaderCallType::kDuringCreation, headers, container_policy,
        request_url, data);
  }

  if (ShouldAddClientHint(data, WebClientHintsType::kPrefersColorScheme)) {
    AddPrefersColorSchemeHeader(headers, frame_tree_node);
  }

  if (ShouldAddClientHint(data, WebClientHintsType::kPrefersReducedMotion)) {
    AddPrefersReducedMotionHeader(headers, frame_tree_node);
  }

  if (ShouldAddClientHint(data, WebClientHintsType::kSaveData))
    AddSaveDataHeader(headers, context);

  // Static assert that triggers if a new client hint header is added. If a
  // new client hint header is added, the following assertion should be updated.
  // If possible, logic should be added above so that the request headers for
  // the newly added client hint can be added to the request.
  static_assert(
      network::mojom::WebClientHintsType::kPrefersReducedMotion ==
          network::mojom::WebClientHintsType::kMaxValue,
      "Consider adding client hint request headers from the browser process");

  // TODO(crbug.com/735518): If the request is redirected, the client hint
  // headers stay attached to the redirected request. Consider removing/adding
  // the client hints headers if the request is redirected with a change in
  // scheme or a change in the origin.
}

}  // namespace

void AddPrefetchNavigationRequestClientHintsHeaders(
    const url::Origin& origin,
    net::HttpRequestHeaders* headers,
    BrowserContext* context,
    ClientHintsControllerDelegate* delegate,
    bool is_ua_override_on,
    bool is_javascript_enabled) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK_EQ(network::kWebEffectiveConnectionTypeMappingCount,
            net::EFFECTIVE_CONNECTION_TYPE_4G + 1u);
  DCHECK_EQ(network::kWebEffectiveConnectionTypeMappingCount,
            static_cast<size_t>(net::EFFECTIVE_CONNECTION_TYPE_LAST));
  DCHECK(context);

  // Since prefetch navigation doesn't have a related frame tree node,
  // |is_javascript_enabled| is passed in to get whether a typical frame tree
  // node would support javascript.
  if (!is_javascript_enabled ||
      !ShouldAddClientHints(origin, nullptr, delegate)) {
    return;
  }

  AddRequestClientHintsHeaders(origin, headers, context, delegate,
                               is_ua_override_on, nullptr, {}, absl::nullopt);
}

void AddNavigationRequestClientHintsHeaders(
    const url::Origin& origin,
    net::HttpRequestHeaders* headers,
    BrowserContext* context,
    ClientHintsControllerDelegate* delegate,
    bool is_ua_override_on,
    FrameTreeNode* frame_tree_node,
    const blink::ParsedPermissionsPolicy& container_policy,
    const absl::optional<GURL>& request_url) {
  DCHECK(frame_tree_node);
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK_EQ(network::kWebEffectiveConnectionTypeMappingCount,
            net::EFFECTIVE_CONNECTION_TYPE_4G + 1u);
  DCHECK_EQ(network::kWebEffectiveConnectionTypeMappingCount,
            static_cast<size_t>(net::EFFECTIVE_CONNECTION_TYPE_LAST));
  DCHECK(context);
  if (!ShouldAddClientHints(origin, frame_tree_node, delegate, request_url)) {
    return;
  }

  AddRequestClientHintsHeaders(origin, headers, context, delegate,
                               is_ua_override_on, frame_tree_node,
                               container_policy, request_url);
}

absl::optional<std::vector<WebClientHintsType>>
ParseAndPersistAcceptCHForNavigation(
    const url::Origin& origin,
    const network::mojom::ParsedHeadersPtr& parsed_headers,
    const net::HttpResponseHeaders* response_headers,
    BrowserContext* context,
    ClientHintsControllerDelegate* delegate,
    FrameTreeNode* frame_tree_node) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK(context);
  DCHECK(parsed_headers);

  if (!parsed_headers->accept_ch)
    return absl::nullopt;

  if (!IsValidURLForClientHints(origin))
    return absl::nullopt;

  // Client hints should only be enabled when JavaScript is enabled. Platforms
  // which enable/disable JavaScript on a per-origin basis should implement
  // IsJavaScriptAllowed to check a given origin. Other platforms (Android
  // WebView) enable/disable JavaScript on a per-View basis, using the
  // WebPreferences setting.
  if (!delegate->IsJavaScriptAllowed(
          origin.GetURL(), frame_tree_node->GetParentOrOuterDocument()) ||
      !IsJavascriptEnabled(frame_tree_node)) {
    return absl::nullopt;
  }

  std::vector<WebClientHintsType> accept_ch = parsed_headers->accept_ch.value();
  url::Origin main_frame_origin = origin;
  absl::optional<url::Origin> third_party_origin;
  // Only the main frame should parse accept-CH, except for the temporary
  // Sec-CH-UA-Reduced client hint (used for the User-Agent reduction origin
  // trial).
  //
  // Note that if Sec-CH-UA-Reduced is persisted for an embedded frame, it
  // means a subsequent top-level navigation will read Sec-CH-UA-Reduced from
  // the Accept-CH cache and send a reduced User-Agent string.
  //
  // TODO(crbug.com/1258063): Delete this call when the UserAgentReduction
  // Origin Trial is finished.
  // TODO(crbug.com/1433353): fix this caller to actually send the outermost
  // frame.
  if (!frame_tree_node->IsMainFrame()) {
    RemoveAllClientHintsExceptOriginTrialHints(
        origin, frame_tree_node, delegate, &accept_ch, &main_frame_origin,
        &third_party_origin);
    if (accept_ch.empty()) {
      // There are is no Sec-CH-UA-Reduced in Accept-CH for the embedded frame,
      // so nothing should be persisted.
      return absl::nullopt;
    }
  }

  absl::optional<GURL> third_party_url = absl::nullopt;
  if (third_party_origin)
    third_party_url = third_party_origin->GetURL();

  blink::EnabledClientHints enabled_hints;
  for (const WebClientHintsType type : accept_ch) {
    enabled_hints.SetIsEnabled(main_frame_origin.GetURL(), third_party_url,
                               response_headers, type, true);
  }

  const std::vector<WebClientHintsType> persisted_hints =
      enabled_hints.GetEnabledHints();
  DCHECK(frame_tree_node);
  PersistAcceptCH(origin, *frame_tree_node, delegate, persisted_hints);
  return persisted_hints;
}

void PersistAcceptCH(const url::Origin& origin,
                     FrameTreeNode& frame_tree_node,
                     ClientHintsControllerDelegate* delegate,
                     const std::vector<WebClientHintsType>& hints) {
  DCHECK(delegate);

  // TODO(https://crbug.com/1355279): Moving the if condition from the caller
  // into this function after PrerenderHost becomes a FrameTreeDelegate. A
  // clearer pattern should be to check whether it is in a prerendering tree
  // and return a nullptr if it isn't. However, the current prerender
  // implementation returns a nullptr in two cases: not prerendered or
  // prerender is canceled, and the callers cannot distinguish between the two
  // reasons and have to have another if condition.
  if (frame_tree_node.frame_tree().is_prerendering()) {
    // For prerendering headers, it should not persist the client header until
    // activation, considering user has not visited the page and allowed it to
    // change content setting yet. The client hints should apply to navigations
    // in the prerendering page, and propagate to the global setting upon user
    // navigation.
    // If host is nullptr, it means prerender has been canceled and will be
    // deleted soon, so we do not need to persist anything.
    if (auto* host =
            PrerenderHost::GetPrerenderHostFromFrameTreeNode(frame_tree_node)) {
      host->OnAcceptClientHintChanged(origin, hints);
    }
    return;
  }

  delegate->PersistClientHints(
      origin, frame_tree_node.GetParentOrOuterDocument(), hints);
}

std::vector<WebClientHintsType> LookupAcceptCHForCommit(
    const url::Origin& origin,
    ClientHintsControllerDelegate* delegate,
    FrameTreeNode* frame_tree_node,
    const absl::optional<GURL>& request_url) {
  std::vector<WebClientHintsType> result;
  if (!ShouldAddClientHints(origin, frame_tree_node, delegate, request_url)) {
    return result;
  }

  const ClientHintsExtendedData data(origin, frame_tree_node, delegate,
                                     request_url);
  std::vector<WebClientHintsType> hints = data.hints.GetEnabledHints();
  if (data.is_embedder_ua_reduced &&
      !base::Contains(hints, WebClientHintsType::kUAReduced)) {
    hints.push_back(WebClientHintsType::kUAReduced);
  }
  if (data.is_embedder_ua_full &&
      !base::Contains(hints, WebClientHintsType::kFullUserAgent)) {
    hints.push_back(WebClientHintsType::kFullUserAgent);
  }
  return hints;
}

bool AreCriticalHintsMissing(
    const url::Origin& origin,
    FrameTreeNode* frame_tree_node,
    ClientHintsControllerDelegate* delegate,
    const std::vector<WebClientHintsType>& critical_hints) {
  ClientHintsExtendedData data(origin, frame_tree_node, delegate,
                               absl::nullopt);

  // Note: these only check for per-hint origin/permissions policy settings, not
  // origin-level or "browser-level" policies like disabiling JS or other
  // features.
  for (auto hint : critical_hints) {
    if (IsClientHintAllowed(data, hint) && !IsClientHintEnabled(data, hint)) {
      return true;
    }
  }

  return false;
}

}  // namespace content