// Copyright 2021 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/loader/navigation_early_hints_manager.h"

#include "base/memory/raw_ref.h"
#include "base/metrics/histogram_functions.h"
#include "base/task/single_thread_task_runner.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/global_request_id.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/browser/url_loader_throttles.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/referrer.h"
#include "mojo/public/cpp/bindings/message.h"
#include "mojo/public/cpp/system/data_pipe_drainer.h"
#include "net/base/load_flags.h"
#include "net/base/schemeful_site.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "net/url_request/url_request_job.h"
#include "services/network/public/cpp/content_security_policy/content_security_policy.h"
#include "services/network/public/cpp/content_security_policy/csp_source_list.h"
#include "services/network/public/cpp/record_ontransfersizeupdate_utils.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#include "services/network/public/mojom/connection_change_observer_client.mojom.h"
#include "services/network/public/mojom/content_security_policy.mojom-shared.h"
#include "services/network/public/mojom/fetch_api.mojom.h"
#include "services/network/public/mojom/network_context.mojom.h"
#include "services/network/public/mojom/url_loader.mojom.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "third_party/blink/public/common/loader/network_utils.h"
#include "third_party/blink/public/common/loader/throttling_url_loader.h"
#include "third_party/blink/public/common/loader/url_loader_throttle.h"
#include "third_party/blink/public/mojom/loader/resource_load_info.mojom-shared.h"
#include "url/origin.h"

namespace content {

namespace {

const net::NetworkTrafficAnnotationTag kEarlyHintsPreloadTrafficAnnotation =
    net::DefineNetworkTrafficAnnotation("early_hints_preload",
                                        R"(
    semantics {
      sender: "Early Hints"
      description:
        "This request is issued during a main frame navigation to "
        "speculatively fetch resources that will likely be used in the frame."
      trigger:
        "A 103 Early Hints HTTP informational response is received during "
        "navigation."
      data:
        "Arbitrary site-controlled data can be included in the URL."
        "Requests may include cookies and site-specific credentials."
      destination: WEBSITE
    }
    policy {
      cookies_allowed: YES
      cookies_store: "user"
      setting:
        "This feature cannot be disabled by Settings. This feature is not "
        "enabled by default yet. TODO(crbug.com/40496584): Update this "
        "description once the feature is ready."
      chrome_policy {
        URLBlocklist {
          URLBlocklist: { entries: '*' }
        }
      }
      chrome_policy {
        URLAllowlist {
          URLAllowlist { }
        }
      }
    }
    comments:
      "Chrome uses this type of request during navigation and it cannot be "
      "disabled. Using either URLBlocklist or URLAllowlist (or a combination "
      "of both) limits the scope of these requests."
)");

network::mojom::CSPDirectiveName LinkAsAttributeToCSPDirective(
    network::mojom::LinkAsAttribute attr) {
  // https://w3c.github.io/webappsec-csp/#csp-directives
  switch (attr) {
    case network::mojom::LinkAsAttribute::kUnspecified:
      return network::mojom::CSPDirectiveName::Unknown;
    case network::mojom::LinkAsAttribute::kImage:
      return network::mojom::CSPDirectiveName::ImgSrc;
    case network::mojom::LinkAsAttribute::kFont:
      return network::mojom::CSPDirectiveName::FontSrc;
    case network::mojom::LinkAsAttribute::kScript:
      return network::mojom::CSPDirectiveName::ScriptSrcElem;
    case network::mojom::LinkAsAttribute::kStyleSheet:
      return network::mojom::CSPDirectiveName::StyleSrcElem;
    case network::mojom::LinkAsAttribute::kFetch:
      return network::mojom::CSPDirectiveName::ConnectSrc;
  }
  NOTREACHED();
}

bool CheckContentSecurityPolicyForPreload(
    const network::mojom::LinkHeaderPtr& link,
    const std::vector<network::mojom::ContentSecurityPolicyPtr>&
        content_security_policies) {
  DCHECK(link->rel == network::mojom::LinkRelAttribute::kPreload ||
         link->rel == network::mojom::LinkRelAttribute::kModulePreload);

  network::mojom::CSPDirectiveName directive =
      LinkAsAttributeToCSPDirective(link->as);

  for (network::mojom::CSPDirectiveName effective_directive = directive;
       effective_directive != network::mojom::CSPDirectiveName::Unknown;
       effective_directive =
           network::CSPFallbackDirective(effective_directive, directive)) {
    for (auto& policy : content_security_policies) {
      const auto& it = policy->directives.find(effective_directive);
      if (it == policy->directives.end()) {
        continue;
      }

      if (!network::CheckCSPSourceList(directive, *it->second, link->href,
                                       *(policy->self_origin),
                                       /*has_followed_redirect=*/false,
                                       /*is_opaque_fenced_frame=*/false)) {
        // TODO(crbug.com/40218207): Report CSP violation once the final
        // response is received.
        return false;
      }
    }
  }

  return true;
}

std::optional<network::mojom::RequestDestination>
LinkAsAttributeToRequestDestination(const network::mojom::LinkHeaderPtr& link) {
  // https://fetch.spec.whatwg.org/#concept-potential-destination-translate
  switch (link->as) {
    case network::mojom::LinkAsAttribute::kUnspecified:
      // For modulepreload, the request destination should be "script" when `as`
      // is not specified.
      // https://html.spec.whatwg.org/multipage/links.html#link-type-modulepreload
      if (link->rel == network::mojom::LinkRelAttribute::kModulePreload) {
        return network::mojom::RequestDestination::kScript;
      }
      return std::nullopt;
    case network::mojom::LinkAsAttribute::kImage:
      return network::mojom::RequestDestination::kImage;
    case network::mojom::LinkAsAttribute::kFont:
      return network::mojom::RequestDestination::kFont;
    case network::mojom::LinkAsAttribute::kScript:
      return network::mojom::RequestDestination::kScript;
    case network::mojom::LinkAsAttribute::kStyleSheet:
      return network::mojom::RequestDestination::kStyle;
    case network::mojom::LinkAsAttribute::kFetch:
      return network::mojom::RequestDestination::kEmpty;
  }
}

network::mojom::RequestMode CalculateRequestMode(
    const network::mojom::LinkHeaderPtr& link) {
  if (link->rel == network::mojom::LinkRelAttribute::kModulePreload) {
    // When fetching a module script, mode is always "cors".
    // https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-single-module-script
    return network::mojom::RequestMode::kCors;
  }

  switch (link->cross_origin) {
    case network::mojom::CrossOriginAttribute::kUnspecified:
      return network::mojom::RequestMode::kNoCors;
    case network::mojom::CrossOriginAttribute::kAnonymous:
    case network::mojom::CrossOriginAttribute::kUseCredentials:
      return network::mojom::RequestMode::kCors;
  }
  NOTREACHED();
}

network::mojom::CredentialsMode CalculateCredentialsMode(
    const network::mojom::LinkHeaderPtr& link) {
  switch (link->cross_origin) {
    case network::mojom::CrossOriginAttribute::kUnspecified:
      // For modulepreload credentials mode should be "same-origin" when
      // `cross-origin` is not specified.
      if (link->rel == network::mojom::LinkRelAttribute::kModulePreload) {
        return network::mojom::CredentialsMode::kSameOrigin;
      } else {
        return network::mojom::CredentialsMode::kInclude;
      }
    case network::mojom::CrossOriginAttribute::kUseCredentials:
      return network::mojom::CredentialsMode::kInclude;
    case network::mojom::CrossOriginAttribute::kAnonymous:
      return network::mojom::CredentialsMode::kSameOrigin;
  }
  NOTREACHED();
}

}  // namespace

// A URLLoaderClient which drains the content of a request to put a
// response into the disk cache. If the response was already in the cache,
// this tries to cancel reading body to avoid further disk access.
class NavigationEarlyHintsManager::PreloadURLLoaderClient
    : public network::mojom::URLLoaderClient,
      public mojo::DataPipeDrainer::Client {
 public:
  PreloadURLLoaderClient(NavigationEarlyHintsManager& owner,
                         const network::ResourceRequest& request)
      : owner_(owner), url_(request.url) {}

  ~PreloadURLLoaderClient() override = default;

  PreloadURLLoaderClient(const PreloadURLLoaderClient&) = delete;
  PreloadURLLoaderClient& operator=(const PreloadURLLoaderClient&) = delete;
  PreloadURLLoaderClient(PreloadURLLoaderClient&&) = delete;
  PreloadURLLoaderClient& operator=(PreloadURLLoaderClient&&) = delete;

 private:
  // mojom::URLLoaderClient overrides:
  void OnReceiveEarlyHints(network::mojom::EarlyHintsPtr early_hints) override {
  }
  void OnReceiveResponse(
      network::mojom::URLResponseHeadPtr head,
      mojo::ScopedDataPipeConsumerHandle body,
      std::optional<mojo_base::BigBuffer> cached_metadata) override {
    if (!head->network_accessed && head->was_fetched_via_cache) {
      // Cancel the client since the response is already stored in the cache.
      result_.was_canceled = true;
      MaybeCompletePreload();
      return;
    }

    if (!body) {
      return;
    }

    if (response_body_drainer_) {
      mojo::ReportBadMessage("NEHM_BAD_RESPONSE_BODY");
      return;
    }
    response_body_drainer_ =
        std::make_unique<mojo::DataPipeDrainer>(this, std::move(body));
  }

#if BUILDFLAG(ARKWEB_RESOURCE_INTERCEPTION)
  void OnTransferDataWithSharedMemory(base::ReadOnlySharedMemoryRegion region, uint64_t buffer_size) override {}
#endif

  void OnReceiveRedirect(const net::RedirectInfo& redirect_info,
                         network::mojom::URLResponseHeadPtr head) override {}
  void OnUploadProgress(int64_t current_position,
                        int64_t total_size,
                        OnUploadProgressCallback callback) override {
    NOTREACHED();
  }
  void OnTransferSizeUpdated(int32_t transfer_size_diff) override {
    network::RecordOnTransferSizeUpdatedUMA(
        network::OnTransferSizeUpdatedFrom::kPreloadURLLoaderClient);
  }
  void OnComplete(const network::URLLoaderCompletionStatus& status) override {
    if (result_.was_canceled || result_.error_code.has_value()) {
      mojo::ReportBadMessage("NEHM_BAD_COMPLETE");
      return;
    }
    result_.error_code = status.error_code;
    result_.cors_error_status = status.cors_error_status;
    MaybeCompletePreload();
  }

  // mojo::DataPipeDrainer::Client overrides:
  void OnDataAvailable(base::span<const uint8_t> data) override {}
  void OnDataComplete() override {
    DCHECK(response_body_drainer_);
    response_body_drainer_.reset();
    MaybeCompletePreload();
  }

  bool CanCompletePreload() {
    if (result_.was_canceled) {
      return true;
    }
    if (result_.error_code.has_value() && !response_body_drainer_) {
      return true;
    }
    return false;
  }

  void MaybeCompletePreload() {
    if (CanCompletePreload()) {
      // Delete `this`.
      owner_->OnPreloadComplete(url_, result_);
    }
  }

  const raw_ref<NavigationEarlyHintsManager> owner_;
  const GURL url_;

  PreloadedResource result_;
  std::unique_ptr<mojo::DataPipeDrainer> response_body_drainer_;
};

NavigationEarlyHintsManagerParams::NavigationEarlyHintsManagerParams(
    const url::Origin& origin,
    net::IsolationInfo isolation_info,
    mojo::Remote<network::mojom::URLLoaderFactory> loader_factory)
    : origin(origin),
      isolation_info(std::move(isolation_info)),
      loader_factory(std::move(loader_factory)) {}

NavigationEarlyHintsManagerParams::~NavigationEarlyHintsManagerParams() =
    default;

NavigationEarlyHintsManagerParams::NavigationEarlyHintsManagerParams(
    NavigationEarlyHintsManagerParams&&) = default;

NavigationEarlyHintsManagerParams& NavigationEarlyHintsManagerParams::operator=(
    NavigationEarlyHintsManagerParams&&) = default;

// Represents a preconnect.
struct NavigationEarlyHintsManager::PreconnectEntry {
  PreconnectEntry(const url::Origin& origin,
                  network::mojom::CrossOriginAttribute cross_origin);
  ~PreconnectEntry();
  PreconnectEntry(const PreconnectEntry&);
  PreconnectEntry& operator=(const PreconnectEntry&);

  bool operator==(const PreconnectEntry&);
  bool operator<(const PreconnectEntry&) const;

  url::Origin origin;
  network::mojom::CrossOriginAttribute cross_origin;
};

NavigationEarlyHintsManager::PreconnectEntry::PreconnectEntry(
    const url::Origin& origin,
    network::mojom::CrossOriginAttribute cross_origin)
    : origin(origin), cross_origin(cross_origin) {}

NavigationEarlyHintsManager::PreconnectEntry::~PreconnectEntry() = default;

NavigationEarlyHintsManager::PreconnectEntry::PreconnectEntry(
    const PreconnectEntry&) = default;

NavigationEarlyHintsManager::PreconnectEntry&
NavigationEarlyHintsManager::PreconnectEntry::operator=(
    const PreconnectEntry&) = default;

bool NavigationEarlyHintsManager::PreconnectEntry::operator==(
    const PreconnectEntry& other) {
  return origin == other.origin && cross_origin == other.cross_origin;
}

bool NavigationEarlyHintsManager::PreconnectEntry::operator<(
    const PreconnectEntry& other) const {
  if (origin == other.origin) {
    return cross_origin < other.cross_origin;
  }
  return origin < other.origin;
}

NavigationEarlyHintsManager::PreloadedResource::PreloadedResource() = default;

NavigationEarlyHintsManager::PreloadedResource::~PreloadedResource() = default;

NavigationEarlyHintsManager::PreloadedResource::PreloadedResource(
    const PreloadedResource&) = default;

NavigationEarlyHintsManager::PreloadedResource&
NavigationEarlyHintsManager::PreloadedResource::operator=(
    const PreloadedResource&) = default;

NavigationEarlyHintsManager::InflightPreload::InflightPreload(
    std::unique_ptr<blink::ThrottlingURLLoader> loader,
    std::unique_ptr<PreloadURLLoaderClient> client)
    : client(std::move(client)), loader(std::move(loader)) {}

NavigationEarlyHintsManager::InflightPreload::~InflightPreload() = default;

NavigationEarlyHintsManager::NavigationEarlyHintsManager(
    BrowserContext& browser_context,
    StoragePartition& storage_partition,
    FrameTreeNodeId frame_tree_node_id,
    NavigationEarlyHintsManagerParams params)
    : browser_context_(browser_context),
      storage_partition_(storage_partition),
      frame_tree_node_id_(frame_tree_node_id),
      loader_factory_(std::move(params.loader_factory)),
      origin_(params.origin),
      isolation_info_(std::move(params.isolation_info)) {
  shared_loader_factory_ =
      base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
          loader_factory_.get());
}

NavigationEarlyHintsManager::~NavigationEarlyHintsManager() = default;

void NavigationEarlyHintsManager::HandleEarlyHints(
    network::mojom::EarlyHintsPtr early_hints,
    const network::ResourceRequest& request_for_navigation) {
  // Ignore the second and subsequent responses to avoid situations where
  // policies such as CSP are inconsistent among the first and following
  // responses. This behavior is specified by the step 19.5 of
  // https://html.spec.whatwg.org/multipage/browsing-the-web.html#create-navigation-params-by-fetching
  if (first_early_hints_receive_time_) {
    return;
  }

  first_early_hints_receive_time_ = base::TimeTicks::Now();

  net::ReferrerPolicy referrer_policy =
      Referrer::ReferrerPolicyForUrlRequest(early_hints->referrer_policy);

  for (const auto& link : early_hints->headers->link_headers) {
    // TODO(crbug.com/40496584): Support other `rel` attributes.
    if (link->rel == network::mojom::LinkRelAttribute::kPreconnect) {
      MaybePreconnect(link);
    } else if (link->rel == network::mojom::LinkRelAttribute::kPreload ||
               link->rel == network::mojom::LinkRelAttribute::kModulePreload) {
      MaybePreloadHintedResource(link, request_for_navigation,
                                 early_hints->headers->content_security_policy,
                                 referrer_policy);
    }
  }
}

bool NavigationEarlyHintsManager::WasResourceHintsReceived() const {
  return was_resource_hints_received_;
}

std::vector<GURL> NavigationEarlyHintsManager::TakePreloadedResourceURLs() {
  return std::move(preloaded_urls_);
}

bool NavigationEarlyHintsManager::HasInflightPreloads() const {
  return inflight_preloads_.size() > 0;
}

void NavigationEarlyHintsManager::WaitForPreloadsFinishedForTesting(
    base::OnceCallback<void(PreloadedResources)> callback) {
  DCHECK(!preloads_completion_callback_for_testing_);
  if (inflight_preloads_.empty()) {
    std::move(callback).Run(preloaded_resources_);
  } else {
    preloads_completion_callback_for_testing_ = std::move(callback);
  }
}

void NavigationEarlyHintsManager::SetNetworkContextForTesting(
    network::mojom::NetworkContext* network_context) {
  DCHECK(!network_context_for_testing_);
  DCHECK(network_context);
  network_context_for_testing_ = network_context;
}

network::mojom::NetworkContext*
NavigationEarlyHintsManager::GetNetworkContext() {
  if (network_context_for_testing_) {
    return network_context_for_testing_;
  }

  return storage_partition_->GetNetworkContext();
}

void NavigationEarlyHintsManager::MaybePreconnect(
    const network::mojom::LinkHeaderPtr& link) {
  was_resource_hints_received_ = true;

  if (!ShouldHandleResourceHints(link)) {
    return;
  }

  PreconnectEntry entry(url::Origin::Create(link->href), link->cross_origin);
  if (preconnect_entries_.contains(entry)) {
    return;
  }

  network::mojom::NetworkContext* network_context = GetNetworkContext();
  if (!network_context) {
    return;
  }

  bool allow_credentials =
      link->cross_origin != network::mojom::CrossOriginAttribute::kAnonymous;
  network_context->PreconnectSockets(
      /*num_streams=*/1, link->href,
      allow_credentials ? network::mojom::CredentialsMode::kInclude
                        : network::mojom::CredentialsMode::kOmit,
      isolation_info_.network_anonymization_key(),
      net::MutableNetworkTrafficAnnotationTag(
          kEarlyHintsPreloadTrafficAnnotation),
      /*keepalive_config=*/std::nullopt, mojo::NullRemote());
  preconnect_entries_.insert(std::move(entry));
}

void NavigationEarlyHintsManager::MaybePreloadHintedResource(
    const network::mojom::LinkHeaderPtr& link,
    const network::ResourceRequest& request_for_navigation,
    const std::vector<network::mojom::ContentSecurityPolicyPtr>&
        content_security_policies,
    net::ReferrerPolicy referrer_policy) {
  DCHECK(request_for_navigation.is_outermost_main_frame);
  DCHECK(request_for_navigation.url.SchemeIsHTTPOrHTTPS());

  was_resource_hints_received_ = true;

  if (!ShouldHandleResourceHints(link)) {
    return;
  }

  // Step 2. If options's destination is not a destination, then return null.
  // https://html.spec.whatwg.org/multipage/semantics.html#create-a-link-request
  std::optional<network::mojom::RequestDestination> destination =
      LinkAsAttributeToRequestDestination(link);
  if (!destination) {
    return;
  }

  if (!CheckContentSecurityPolicyForPreload(link, content_security_policies)) {
    return;
  }

  if (inflight_preloads_.contains(link->href) ||
      preloaded_resources_.contains(link->href)) {
    return;
  }

  auto preload_origin = url::Origin::Create(link->href);

  net::SiteForCookies site_for_cookies =
      net::SiteForCookies::FromOrigin(origin_);
  network::ResourceRequest request;
  request.method = net::HttpRequestHeaders::kGetMethod;
  request.priority = CalculateRequestPriority(link);
  request.destination = *destination;
  request.url = link->href;
  request.site_for_cookies = site_for_cookies;
  request.request_initiator = origin_;
  request.referrer = net::URLRequestJob::ComputeReferrerForPolicy(
      referrer_policy, request_for_navigation.url, request.url);
  request.referrer_policy = referrer_policy;
  request.load_flags = net::LOAD_NORMAL;
  request.resource_type =
      static_cast<int>(blink::mojom::ResourceType::kSubResource);
  request.mode = CalculateRequestMode(link);
  request.credentials_mode = CalculateCredentialsMode(link);

  blink::network_utils::SetAcceptHeader(request.headers, request.destination);

  std::vector<std::unique_ptr<blink::URLLoaderThrottle>> throttles =
      CreateContentBrowserURLLoaderThrottles(
          request, &*browser_context_,
          base::BindRepeating(&WebContents::FromFrameTreeNodeId,
                              frame_tree_node_id_),
          /*navigation_ui_data=*/nullptr, frame_tree_node_id_,
          /*navigation_id=*/std::nullopt);

  auto loader_client = std::make_unique<PreloadURLLoaderClient>(*this, request);
  auto loader = blink::ThrottlingURLLoader::CreateLoaderAndStart(
      shared_loader_factory_, std::move(throttles),
      content::GlobalRequestID::MakeBrowserInitiated().request_id,
      network::mojom::kURLLoadOptionNone, &request, loader_client.get(),
      kEarlyHintsPreloadTrafficAnnotation,
      base::SingleThreadTaskRunner::GetCurrentDefault());

  inflight_preloads_[request.url] = std::make_unique<InflightPreload>(
      std::move(loader), std::move(loader_client));

  preloaded_urls_.push_back(request.url);
}

bool NavigationEarlyHintsManager::ShouldHandleResourceHints(
    const network::mojom::LinkHeaderPtr& link) {
  if (!link->href.SchemeIsHTTPOrHTTPS()) {
    return false;
  }
  return true;
}

void NavigationEarlyHintsManager::OnPreloadComplete(
    const GURL& url,
    const PreloadedResource& result) {
  DCHECK(inflight_preloads_.contains(url));
  DCHECK(!preloaded_resources_.contains(url));
  preloaded_resources_[url] = result;
  inflight_preloads_.erase(url);

  if (inflight_preloads_.empty() && preloads_completion_callback_for_testing_) {
    std::move(preloads_completion_callback_for_testing_)
        .Run(preloaded_resources_);
  }

  // TODO(crbug.com/40496584): Consider to delete `this` when there is no
  // inflight preloads.
}

// Used to determine a priority for a speculative subresource request.
// TODO(crbug.com/40496584): This is almost the same as GetRequestPriority() in
// loading_predictor_tab_helper.cc and the purpose is the same. Consider merging
// them if the logic starts to be more mature.
// platform/loader/fetch/README.md in blink contains more details on
// prioritization as well as links to all of the relevant places in the code
// where priority is determined. If the priority logic is updated here, be sure
// to update the other code as needed.
net::RequestPriority NavigationEarlyHintsManager::CalculateRequestPriority(
    const network::mojom::LinkHeaderPtr& link) {
  // When fetchPriority is explicitly specified for preload, independent of
  // most content types, the blink priority matches the fetchpriority value.
  // In net priority terms that maps to MEDIUM for "high" LOWEST for "low".
  // https://web.dev/priority-hints/#browser-priority-and-fetchpriority
  switch (link->fetch_priority) {
    case network::mojom::FetchPriorityAttribute::kHigh:
      switch (link->as) {
        case network::mojom::LinkAsAttribute::kStyleSheet:
          return net::HIGHEST;
        default:
          return net::MEDIUM;
      }
    case network::mojom::FetchPriorityAttribute::kLow:
      return net::LOWEST;
    case network::mojom::FetchPriorityAttribute::kAuto:
      switch (link->as) {
        case network::mojom::LinkAsAttribute::kStyleSheet:
          return net::HIGHEST;
        case network::mojom::LinkAsAttribute::kFont:
        case network::mojom::LinkAsAttribute::kScript:
          return net::MEDIUM;
        case network::mojom::LinkAsAttribute::kImage:
        case network::mojom::LinkAsAttribute::kFetch:
          return net::LOWEST;
        case network::mojom::LinkAsAttribute::kUnspecified:
          return net::IDLE;
      }
  }
  NOTREACHED();
}

}  // namespace content