// Copyright 2022 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/reduce_accept_language/reduce_accept_language_throttle.h"

#include <algorithm>

#include "base/metrics/histogram_functions.h"
#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/browser/renderer_host/navigation_request.h"
#include "content/public/browser/origin_trials_controller_delegate.h"
#include "content/public/browser/reduce_accept_language_controller_delegate.h"
#include "content/public/browser/reduce_accept_language_utils.h"
#include "services/network/public/cpp/features.h"
#include "third_party/blink/public/common/loader/url_loader_throttle.h"

namespace content {

namespace {

// Metrics on the count of requests restarted or the reason why not restarted
// when reducing accept-language HTTP header. These values are persisted to
// logs. Entries should not be renumbered and numeric values should never be
// reused.
enum class AcceptLanguageNegotiationRestart {
  kNavigationStarted = 0,
  kAvailLanguageAndContentLanguageHeaderPresent = 1,
  kServiceWorkerPreloadRequest = 2,
  kNavigationRestarted = 3,
  kMaxValue = kNavigationRestarted,
};

// Logging the metric related to reduce accept language header and
// corresponding restart metric when language negotiation happens.
void LogAcceptLanguageStatus(AcceptLanguageNegotiationRestart status) {
  base::UmaHistogramEnumeration(
      "ReduceAcceptLanguage.AcceptLanguageNegotiationRestart", status);
}

}  // namespace

ReduceAcceptLanguageThrottle::ReduceAcceptLanguageThrottle(
    ReduceAcceptLanguageUtils reduce_accept_language_utils,
    OriginTrialsControllerDelegate* origin_trials_delegate,
    FrameTreeNodeId frame_tree_node_id)
    : reduce_accept_language_utils_(std::move(reduce_accept_language_utils)),
      origin_trials_delegate_(origin_trials_delegate),
      frame_tree_node_id_(frame_tree_node_id) {
  LogAcceptLanguageStatus(AcceptLanguageNegotiationRestart::kNavigationStarted);
}

ReduceAcceptLanguageThrottle::~ReduceAcceptLanguageThrottle() = default;

void ReduceAcceptLanguageThrottle::WillStartRequest(
    network::ResourceRequest* request,
    bool* defer) {
  last_request_url_ = request->url;
  initial_request_headers_ = request->headers;
}

void ReduceAcceptLanguageThrottle::BeforeWillRedirectRequest(
    const net::RedirectInfo& redirect_info,
    const network::mojom::URLResponseHead& response_head,
    RestartWithURLReset* restart_with_url_reset) {
  // For redirect case, checking if a redirect response should result in a
  // restart of the last requested URL with a better negotiation language,
  // rather than following the redirect.
  //
  // Suppose origin A returns a response redirecting to origin B,
  // `last_request_url_` will be A, and `response_head` contains
  // Content-Language and Avail-Language headers suggesting whether we should
  // follow the redirect response. If the response shows it supports one of
  // user's other preferred language and needs to restart. We will restart the
  // request to A with the new preferred language and expect A responses a
  // different redirect. For detail example, see
  // https://github.com/Tanych/accept-language/issues/3.
  MaybeRestartWithLanguageNegotiation(response_head, restart_with_url_reset);
  // Update the url with the redirect new url to make sure last_request_url_
  // with be the response_url.
  last_request_url_ = redirect_info.new_url;
}

void ReduceAcceptLanguageThrottle::BeforeWillProcessResponse(
    const GURL& response_url,
    const network::mojom::URLResponseHead& response_head,
    RestartWithURLReset* restart_with_url_reset) {
  DCHECK_EQ(response_url, last_request_url_);
  MaybeRestartWithLanguageNegotiation(response_head, restart_with_url_reset);
}

void ReduceAcceptLanguageThrottle::MaybeRestartWithLanguageNegotiation(
    const network::mojom::URLResponseHead& response_head,
    RestartWithURLReset* restart_with_url_reset) {
  // For responses that don't contains content-language and avail-language
  // header, we skip language negotiation for them since we don't know whether
  // we can get a better representation.
  if (!response_head.parsed_headers ||
      !response_head.parsed_headers->content_language ||
      !response_head.parsed_headers->avail_language) {
    return;
  }

  LogAcceptLanguageStatus(AcceptLanguageNegotiationRestart::
                              kAvailLanguageAndContentLanguageHeaderPresent);

  // Skip restart when it's a service worker navigation preload request,
  // otherwise request for the same origin can't guarantee restart at most once.
  // All URLLoaderThrottles instances get recreated and `restarted_origins_` get
  // reset when requests are the service worker preload requests. This could
  // lead to browsers resending too many requests if
  // `ParseAndPersistAcceptLanguageForNavigation` returns need to restart.
  if (response_head.did_service_worker_navigation_preload) {
    LogAcceptLanguageStatus(
        AcceptLanguageNegotiationRestart::kServiceWorkerPreloadRequest);
    return;
  }

  url::Origin last_request_origin = url::Origin::Create(last_request_url_);
  if (!ReduceAcceptLanguageUtils::OriginCanReduceAcceptLanguage(
          last_request_origin)) {
    return;
  }

  FrameTreeNode* frame_tree_node =
      FrameTreeNode::GloballyFindByID(frame_tree_node_id_);
  // Skip if origin opted-in ReduceAcceptLanguage deprecation origin trial.
  if (ReduceAcceptLanguageUtils::CheckDisableReduceAcceptLanguageOriginTrial(
          last_request_url_, frame_tree_node, origin_trials_delegate_)) {
    return;
  }

  // Only restart once per-Origin (per navigation).
  if (!restarted_origins_.insert(last_request_origin).second)
    return;

  bool need_restart =
      reduce_accept_language_utils_.ReadAndPersistAcceptLanguageForNavigation(
          last_request_origin, initial_request_headers_,
          response_head.parsed_headers);

  // Only restart if the initial accept language doesn't match content language.
  if (need_restart) {
    LogAcceptLanguageStatus(
        AcceptLanguageNegotiationRestart::kNavigationRestarted);
    // RestartWithURLResetAndFlags will restart from the original requested URL.
    // Ideally, we expect only restart last requested URL to avoid unnecessary
    // restarts starting from the original request URL. However, for cross
    // origin redirects, it won't pass the SiteForCookies equivalent check on
    // URLLoader when using RestartWithFlags.
    *restart_with_url_reset = RestartWithURLReset(true);
    return;
  }
}

}  // namespace content
