// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/navigation_interception/intercept_navigation_throttle.h"

#include "base/bind.h"
#include "base/metrics/histogram_macros.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/time/time.h"
#include "base/timer/elapsed_timer.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/navigation_handle.h"
#include "url/gurl.h"

namespace navigation_interception {

const base::Feature InterceptNavigationThrottle::kAsyncCheck{
    "AsyncNavigationIntercept", base::FEATURE_DISABLED_BY_DEFAULT};

InterceptNavigationThrottle::InterceptNavigationThrottle(
    content::NavigationHandle* navigation_handle,
    CheckCallback should_ignore_callback)
    : content::NavigationThrottle(navigation_handle),
      should_ignore_callback_(should_ignore_callback),
      ui_task_runner_(base::ThreadTaskRunnerHandle::Get()),
      weak_factory_(this) {}

InterceptNavigationThrottle::~InterceptNavigationThrottle() {
  UMA_HISTOGRAM_BOOLEAN("Navigation.Intercept.Ignored", should_ignore_);
}

content::NavigationThrottle::ThrottleCheckResult
InterceptNavigationThrottle::WillStartRequest() {
  DCHECK(!should_ignore_);
  base::ElapsedTimer timer;

  auto result = CheckIfShouldIgnoreNavigation(false /* is_redirect */);
  UMA_HISTOGRAM_COUNTS_10M("Navigation.Intercept.WillStart",
                           timer.Elapsed().InMicroseconds());
  return result;
}

content::NavigationThrottle::ThrottleCheckResult
InterceptNavigationThrottle::WillRedirectRequest() {
  if (should_ignore_)
    return content::NavigationThrottle::CANCEL_AND_IGNORE;
  return CheckIfShouldIgnoreNavigation(true /* is_redirect */);
}

content::NavigationThrottle::ThrottleCheckResult
InterceptNavigationThrottle::WillFailRequest() {
  return WillFinish();
}

content::NavigationThrottle::ThrottleCheckResult
InterceptNavigationThrottle::WillProcessResponse() {
  return WillFinish();
}

const char* InterceptNavigationThrottle::GetNameForLogging() {
  return "InterceptNavigationThrottle";
}

content::NavigationThrottle::ThrottleCheckResult
InterceptNavigationThrottle::WillFinish() {
  DCHECK(!deferring_);
  if (should_ignore_)
    return content::NavigationThrottle::CANCEL_AND_IGNORE;

  if (pending_checks_ > 0) {
    deferring_ = true;
    return content::NavigationThrottle::DEFER;
  }

  return content::NavigationThrottle::PROCEED;
}

content::NavigationThrottle::ThrottleCheckResult
InterceptNavigationThrottle::CheckIfShouldIgnoreNavigation(bool is_redirect) {
  if (ShouldCheckAsynchronously()) {
    pending_checks_++;
    ui_task_runner_->PostTask(
        FROM_HERE, base::BindOnce(&InterceptNavigationThrottle::RunCheckAsync,
                                  weak_factory_.GetWeakPtr(),
                                  GetNavigationParams(is_redirect)));
    return content::NavigationThrottle::PROCEED;
  }
  // No need to set |should_ignore_| since if it is true, we'll cancel the
  // navigation immediately.
  return should_ignore_callback_.Run(navigation_handle()->GetWebContents(),
                                     GetNavigationParams(is_redirect))
             ? content::NavigationThrottle::CANCEL_AND_IGNORE
             : content::NavigationThrottle::PROCEED;
  // Careful, |this| can be deleted at this point.
}

void InterceptNavigationThrottle::RunCheckAsync(
    const NavigationParams& params) {
  DCHECK(base::FeatureList::IsEnabled(kAsyncCheck));
  DCHECK_GT(pending_checks_, 0);
  pending_checks_--;
  bool final_deferred_check = deferring_ && pending_checks_ == 0;
  auto weak_this = weak_factory_.GetWeakPtr();
  bool should_ignore = should_ignore_callback_.Run(
      navigation_handle()->GetWebContents(), params);
  if (!weak_this)
    return;

  should_ignore_ |= should_ignore;
  if (!final_deferred_check)
    return;

  if (should_ignore) {
    CancelDeferredNavigation(content::NavigationThrottle::CANCEL_AND_IGNORE);
  } else {
    Resume();
  }
}

bool InterceptNavigationThrottle::ShouldCheckAsynchronously() const {
  // Do not apply the async optimization for:
  // - POST navigations, to ensure we aren't violating idempotency.
  // - Subframe navigations, which aren't observed on Android, and should be
  //   fast on other platforms.
  // - non-http/s URLs, which are more likely to be intercepted.
  return navigation_handle()->IsInMainFrame() &&
         !navigation_handle()->IsPost() &&
         navigation_handle()->GetURL().SchemeIsHTTPOrHTTPS() &&
         base::FeatureList::IsEnabled(kAsyncCheck);
}

NavigationParams InterceptNavigationThrottle::GetNavigationParams(
    bool is_redirect) const {
  return NavigationParams(
      navigation_handle()->GetURL(), navigation_handle()->GetReferrer(),
      navigation_handle()->HasUserGesture(), navigation_handle()->IsPost(),
      navigation_handle()->GetPageTransition(), is_redirect,
      navigation_handle()->IsExternalProtocol(), true,
      navigation_handle()->GetBaseURLForDataURL());
}

}  // namespace navigation_interception
