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

#include "chrome/browser/ui/blocked_content/tab_under_navigation_throttle.h"

#include <cmath>
#include <string>
#include <utility>

#include "base/check.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/memory/ptr_util.h"
#include "base/strings/stringprintf.h"
#include "build/build_config.h"
#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/pref_names.h"
#include "components/blocked_content/popup_opener_tab_helper.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "components/content_settings/core/common/content_settings.h"
#include "components/content_settings/core/common/content_settings_types.h"
#include "components/tabs/public/tab_interface.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/visibility.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_delegate.h"
#include "extensions/buildflags/buildflags.h"
#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/metrics/public/cpp/ukm_recorder.h"
#include "services/metrics/public/cpp/ukm_source_id.h"
#include "third_party/blink/public/mojom/devtools/console_message.mojom.h"
#include "url/gurl.h"

#if BUILDFLAG(IS_ANDROID)
#include "chrome/browser/android/framebust_intervention/framebust_blocked_delegate_android.h"
#else
#include "chrome/browser/ui/blocked_content/framebust_block_tab_helper.h"
#endif

#if BUILDFLAG(ENABLE_EXTENSIONS_CORE)
#include "extensions/common/constants.h"
#endif

namespace {

void LogTabUnderAttempt(content::NavigationHandle* handle) {
  // The source id should generally be set, except for very rare circumstances
  // where the popup opener tab helper is not observing at the time the
  // previous navigation commit.
  ukm::UkmRecorder* ukm_recorder = ukm::UkmRecorder::Get();
  ukm::SourceId opener_source_id =
      handle->GetWebContents()->GetPrimaryMainFrame()->GetPageUkmSourceId();
  if (opener_source_id != ukm::kInvalidSourceId && ukm_recorder) {
    ukm::builders::AbusiveExperienceHeuristic_TabUnder(opener_source_id)
        .SetDidTabUnder(true)
        .Record(ukm_recorder);
  }
}

}  // namespace

// static
void TabUnderNavigationThrottle::MaybeCreateAndAdd(
    content::NavigationThrottleRegistry& registry) {
  // TODO(crbug.com/40187173): TabUnderNavigationThrottle doesn't block
  // prerendering activations. However, currently prerender is same-origin only
  // so a prerendered activation could never be classified as a tab-under.
  // Otherwise, it should be safe to avoid creating a throttle in non primary
  // pages because prerendered pages should not be able to open popups. A
  // tab-under could therefore never occur within the non-primary page.
  if (registry.GetNavigationHandle().IsInPrimaryMainFrame()) {
    registry.AddThrottle(
        base::WrapUnique(new TabUnderNavigationThrottle(registry)));
  }
}

TabUnderNavigationThrottle::~TabUnderNavigationThrottle() = default;

TabUnderNavigationThrottle::TabUnderNavigationThrottle(
    content::NavigationThrottleRegistry& registry)
    : content::NavigationThrottle(registry),
      has_opened_popup_since_last_user_gesture_at_start_(
          HasOpenedPopupSinceLastUserGesture()),
      started_in_foreground_(
          registry.GetNavigationHandle().GetWebContents()->GetVisibility() ==
          content::Visibility::VISIBLE) {}

bool TabUnderNavigationThrottle::IsSuspiciousClientRedirect() const {
  // This throttle is only created for primary main frame navigations. See
  // MaybeCreate().
  DCHECK(navigation_handle()->IsInPrimaryMainFrame());
  DCHECK(!navigation_handle()->HasCommitted());

  // Some browser initiated navigations have HasUserGesture set to false. This
  // should eventually be fixed in crbug.com/41257523. In the meantime, just
  // dont block browser initiated ones.
  if (started_in_foreground_ || navigation_handle()->HasUserGesture() ||
      !navigation_handle()->IsRendererInitiated()) {
    return false;
  }

  // An empty previous URL indicates this was the first load. We filter these
  // out because we're primarily interested in sites which navigate themselves
  // away while in the background.
  content::WebContents* contents = navigation_handle()->GetWebContents();
  const GURL& previous_main_frame_url = contents->GetLastCommittedURL();
  if (previous_main_frame_url.is_empty()) {
    return false;
  }

  // Same-site navigations are exempt from tab-under protection.
  const GURL& target_url = navigation_handle()->GetURL();
  if (net::registry_controlled_domains::SameDomainOrHost(
          previous_main_frame_url, target_url,
          net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES)) {
    return false;
  }

#if BUILDFLAG(ENABLE_EXTENSIONS_CORE)
  // Exempt navigating to or from extension URLs, as they will redirect pages in
  // the background. By exempting in both directions, extensions can always
  // round-trip a page through an extension URL in order to perform arbitrary
  // redirections with content scripts.
  if (target_url.SchemeIs(extensions::kExtensionScheme) ||
      previous_main_frame_url.SchemeIs(extensions::kExtensionScheme)) {
    return false;
  }
#endif
  return true;
}

content::NavigationThrottle::ThrottleCheckResult
TabUnderNavigationThrottle::MaybeBlockNavigation() {
  if (seen_tab_under_ || !has_opened_popup_since_last_user_gesture_at_start_ ||
      !IsSuspiciousClientRedirect()) {
    return content::NavigationThrottle::PROCEED;
  }

  seen_tab_under_ = true;

  LogTabUnderAttempt(navigation_handle());

  // We unconditionally proceed. There used to be a tab-under blocking
  // experiment, but it never launched.
  return content::NavigationThrottle::PROCEED;
}

void TabUnderNavigationThrottle::ShowUI() {
  content::WebContents* web_contents = navigation_handle()->GetWebContents();
  const GURL& url = navigation_handle()->GetURL();
#if BUILDFLAG(IS_ANDROID)
  blocked_content::FramebustBlockedMessageDelegate::CreateForWebContents(
      web_contents);
  blocked_content::FramebustBlockedMessageDelegate*
      framebust_blocked_message_delegate =
          blocked_content::FramebustBlockedMessageDelegate::FromWebContents(
              web_contents);
  framebust_blocked_message_delegate->ShowMessage(
      url, navigation_handle()->GetInitiatorOrigin(),
      HostContentSettingsMapFactory::GetForProfile(
          web_contents->GetBrowserContext()),
      base::NullCallback());
#else
  tabs::TabInterface* tab =
      tabs::TabInterface::MaybeGetFromContents(web_contents);
  if (auto* tab_helper = tab ? FramebustBlockTabHelper::From(tab) : nullptr) {
    tab_helper->AddBlockedUrl(url, navigation_handle()->GetInitiatorOrigin(),
                              base::NullCallback());
  }
#endif
}

bool TabUnderNavigationThrottle::HasOpenedPopupSinceLastUserGesture() const {
  content::WebContents* contents = navigation_handle()->GetWebContents();
  auto* popup_opener =
      blocked_content::PopupOpenerTabHelper::FromWebContents(contents);
  return popup_opener &&
         popup_opener->has_opened_popup_since_last_user_gesture();
}

content::NavigationThrottle::ThrottleCheckResult
TabUnderNavigationThrottle::WillStartRequest() {
  return MaybeBlockNavigation();
}

content::NavigationThrottle::ThrottleCheckResult
TabUnderNavigationThrottle::WillRedirectRequest() {
  return MaybeBlockNavigation();
}

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