// 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 "components/security_interstitials/content/security_interstitial_tab_helper.h"

#include <memory>

#include "base/functional/bind.h"
#include "base/i18n/rtl.h"
#include "base/memory/raw_ptr.h"
#include "base/test/scoped_feature_list.h"
#include "base/time/time.h"
#include "components/security_interstitials/content/security_interstitial_controller_client.h"
#include "components/security_interstitials/content/security_interstitial_page.h"
#include "components/security_interstitials/content/settings_page_helper.h"
#include "components/security_interstitials/core/controller_client.h"
#include "components/security_interstitials/core/metrics_helper.h"
#include "content/public/browser/certificate_request_result_type.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/test/mock_navigation_handle.h"
#include "content/public/test/navigation_simulator.h"
#include "content/public/test/test_renderer_host.h"
#include "net/base/net_errors.h"
#include "net/test/cert_test_util.h"
#include "net/test/test_data_directory.h"
#include "third_party/blink/public/common/features.h"
#include "url/gurl.h"

namespace security_interstitials {

const char kTestSslMetricsName[] = "test_blocking_page";

std::unique_ptr<security_interstitials::MetricsHelper> CreateTestMetricsHelper(
    content::WebContents* web_contents) {
  MetricsHelper::ReportDetails report_details;
  report_details.metric_prefix = kTestSslMetricsName;
  return std::make_unique<security_interstitials::MetricsHelper>(
      GURL(), report_details, nullptr);
}

class TestInterstitialPage : public SecurityInterstitialPage {
 public:
  // |*destroyed_tracker| is set to true in the destructor.
  TestInterstitialPage(content::WebContents* web_contents,
                       const GURL& request_url,
                       bool* destroyed_tracker)
      : SecurityInterstitialPage(
            web_contents,
            request_url,
            std::make_unique<SecurityInterstitialControllerClient>(
                web_contents,
                CreateTestMetricsHelper(web_contents),
                nullptr,
                base::i18n::GetConfiguredLocale(),
                GURL(),
                /* settings_page_helper*/ nullptr)),
        destroyed_tracker_(destroyed_tracker) {}

  ~TestInterstitialPage() override { *destroyed_tracker_ = true; }

  void OnInterstitialClosing() override {}

 protected:
  void PopulateInterstitialStrings(base::DictValue& load_time_data) override {}

 private:
  raw_ptr<bool> destroyed_tracker_;
};

class SecurityInterstitialTabHelperTest
    : public content::RenderViewHostTestHarness {
 protected:
  void SetUp() override {
    RenderViewHostTestHarness::SetUp();
    content::RenderFrameHostTester::For(main_rfh())
        ->InitializeRenderFrameIfNeeded();
  }

  // Creates a navigation handle for the main frame.
  std::unique_ptr<content::MockNavigationHandle> CreateHandle(
      bool committed,
      bool is_same_document) {
    return CreateHandle(main_rfh(), committed, is_same_document);
  }

  // Creates a navigation handle for a subframe.
  std::unique_ptr<content::MockNavigationHandle> CreateSubframeHandle(
      bool committed,
      bool is_same_document) {
    content::RenderFrameHost* subframe =
        content::RenderFrameHostTester::For(main_rfh())
            ->AppendChild("subframe");
    return CreateHandle(subframe, committed, is_same_document);
  }

  // Creates a navigation handle for the given frame.
  std::unique_ptr<content::MockNavigationHandle> CreateHandle(
      content::RenderFrameHost* frame,
      bool committed,
      bool is_same_document) {
    std::unique_ptr<content::MockNavigationHandle> handle =
        std::make_unique<content::MockNavigationHandle>(GURL(), frame);
    handle->set_has_committed(committed);
    handle->set_is_same_document(is_same_document);
    return handle;
  }

  // The lifetime of the blocking page is managed by the
  // SecurityInterstitialTabHelper for the test's web_contents.
  // |destroyed_tracker| will be set to true when the corresponding blocking
  // page is destroyed.
  void CreateAssociatedBlockingPage(content::NavigationHandle* handle,
                                    bool* destroyed_tracker) {
    SecurityInterstitialTabHelper::AssociateBlockingPage(
        handle, std::make_unique<TestInterstitialPage>(web_contents(), GURL(),
                                                       destroyed_tracker));
  }
};

// Tests that the helper properly handles the lifetime of a single blocking
// page, interleaved with other navigations.
TEST_F(SecurityInterstitialTabHelperTest, SingleBlockingPage) {
  std::unique_ptr<content::MockNavigationHandle> blocking_page_handle =
      CreateHandle(true, false);
  bool blocking_page_destroyed = false;
  CreateAssociatedBlockingPage(blocking_page_handle.get(),
                               &blocking_page_destroyed);
  SecurityInterstitialTabHelper* helper =
      SecurityInterstitialTabHelper::FromWebContents(web_contents());

  // Test that a same-document navigation doesn't destroy the blocking page if
  // its navigation hasn't committed yet.
  std::unique_ptr<content::MockNavigationHandle> same_document_handle =
      CreateHandle(true, true);
  helper->DidFinishNavigation(same_document_handle.get());
  EXPECT_FALSE(blocking_page_destroyed);

  // Test that a committed (non-same-document) navigation doesn't destroy the
  // blocking page if its navigation hasn't committed yet.
  std::unique_ptr<content::MockNavigationHandle> committed_handle1 =
      CreateHandle(true, false);
  helper->DidFinishNavigation(committed_handle1.get());
  EXPECT_FALSE(blocking_page_destroyed);

  // Simulate comitting the interstitial.
  helper->DidFinishNavigation(blocking_page_handle.get());
  EXPECT_FALSE(blocking_page_destroyed);

  // Test that a subsequent committed navigation releases the blocking page
  // stored for the currently committed navigation.
  std::unique_ptr<content::MockNavigationHandle> committed_handle2 =
      CreateHandle(true, false);
  helper->DidFinishNavigation(committed_handle2.get());
  EXPECT_TRUE(blocking_page_destroyed);
}

// Tests that the helper properly handles the lifetime of multiple blocking
// pages for a single FrameTreeNode, committed in a different order than they
// are created.
TEST_F(SecurityInterstitialTabHelperTest, MultipleBlockingPages) {
  // Simulate associating the first interstitial.
  std::unique_ptr<content::MockNavigationHandle> handle1 =
      CreateHandle(true, false);
  bool blocking_page1_destroyed = false;
  CreateAssociatedBlockingPage(handle1.get(), &blocking_page1_destroyed);

  // We can directly retrieve the helper for testing once
  // CreateAssociatedBlockingPage() was called.
  SecurityInterstitialTabHelper* helper =
      SecurityInterstitialTabHelper::FromWebContents(web_contents());

  // Simulate committing the first interstitial.
  helper->DidFinishNavigation(handle1.get());
  EXPECT_FALSE(blocking_page1_destroyed);
  EXPECT_TRUE(helper->IsDisplayingInterstitial());

  // Associate the second interstitial.
  std::unique_ptr<content::MockNavigationHandle> handle2 =
      CreateHandle(true, false);
  bool blocking_page2_destroyed = false;
  CreateAssociatedBlockingPage(handle2.get(), &blocking_page2_destroyed);
  EXPECT_FALSE(blocking_page1_destroyed);
  EXPECT_FALSE(blocking_page2_destroyed);

  // Associate the third interstitial.
  std::unique_ptr<content::MockNavigationHandle> handle3 =
      CreateHandle(true, false);
  bool blocking_page3_destroyed = false;
  CreateAssociatedBlockingPage(handle3.get(), &blocking_page3_destroyed);
  EXPECT_FALSE(blocking_page1_destroyed);
  EXPECT_FALSE(blocking_page2_destroyed);
  EXPECT_FALSE(blocking_page3_destroyed);

  // Simulate committing the third interstitial.
  helper->DidFinishNavigation(handle3.get());
  EXPECT_TRUE(blocking_page1_destroyed);
  EXPECT_FALSE(blocking_page2_destroyed);
  EXPECT_FALSE(blocking_page3_destroyed);

  // Simulate commiting the second interstitial.
  helper->DidFinishNavigation(handle2.get());
  EXPECT_TRUE(blocking_page1_destroyed);
  EXPECT_FALSE(blocking_page2_destroyed);
  EXPECT_TRUE(blocking_page3_destroyed);

  // Test that a subsequent committed navigation releases the last blocking
  // page.
  std::unique_ptr<content::MockNavigationHandle> committed_handle4 =
      CreateHandle(true, false);
  helper->DidFinishNavigation(committed_handle4.get());
  EXPECT_TRUE(blocking_page2_destroyed);
}

// Tests that the helper properly handles a navigation that finishes without
// committing.
TEST_F(SecurityInterstitialTabHelperTest, NavigationDoesNotCommit) {
  std::unique_ptr<content::MockNavigationHandle> committed_handle =
      CreateHandle(true, false);
  bool committed_blocking_page_destroyed = false;
  CreateAssociatedBlockingPage(committed_handle.get(),
                               &committed_blocking_page_destroyed);
  SecurityInterstitialTabHelper* helper =
      SecurityInterstitialTabHelper::FromWebContents(web_contents());
  helper->DidFinishNavigation(committed_handle.get());
  EXPECT_FALSE(committed_blocking_page_destroyed);

  // Simulate a navigation that does not commit.
  std::unique_ptr<content::MockNavigationHandle> non_committed_handle =
      CreateHandle(false, false);
  helper->DidFinishNavigation(non_committed_handle.get());

  // The blocking page for the non-committed navigation should have been cleaned
  // up, but the one for the previous committed navigation should still be
  // around.
  EXPECT_FALSE(committed_blocking_page_destroyed);

  // When a navigation does commit, the previous one should be cleaned up.
  std::unique_ptr<content::MockNavigationHandle> next_committed_handle =
      CreateHandle(true, false);
  helper->DidFinishNavigation(next_committed_handle.get());
  EXPECT_TRUE(committed_blocking_page_destroyed);
}

// Tests that main frame committed blocking interstitial is cleaned up on tab
// closing.
TEST_F(SecurityInterstitialTabHelperTest, ClosingTabForMainFrameInterstitial) {
  std::unique_ptr<content::MockNavigationHandle> committed_handle =
      CreateHandle(true, false);
  bool committed_blocking_page_destroyed = false;
  CreateAssociatedBlockingPage(committed_handle.get(),
                               &committed_blocking_page_destroyed);
  SecurityInterstitialTabHelper* helper =
      SecurityInterstitialTabHelper::FromWebContents(web_contents());
  helper->DidFinishNavigation(committed_handle.get());
  EXPECT_FALSE(committed_blocking_page_destroyed);

  // Only subframe interstitials are destroyed on frame deletion.
  helper->FrameDeleted(main_rfh()->GetFrameTreeNodeId());
  EXPECT_FALSE(committed_blocking_page_destroyed);

  helper->WebContentsDestroyed();
  EXPECT_TRUE(committed_blocking_page_destroyed);
}

// Tests that multiple FrameTreeNodes can display blocking interstitials.
TEST_F(SecurityInterstitialTabHelperTest, MultipleFramesBlocked) {
  // Simulate associating the first interstitial.
  std::unique_ptr<content::MockNavigationHandle> handle1 =
      CreateSubframeHandle(true, false);
  bool blocking_page1_destroyed = false;
  CreateAssociatedBlockingPage(handle1.get(), &blocking_page1_destroyed);

  // Simulate associating the second interstitial to another frame.
  std::unique_ptr<content::MockNavigationHandle> handle2 =
      CreateSubframeHandle(true, false);
  bool blocking_page2_destroyed = false;
  CreateAssociatedBlockingPage(handle2.get(), &blocking_page2_destroyed);

  // We can directly retrieve the helper for testing once
  // CreateAssociatedBlockingPage() was called.
  SecurityInterstitialTabHelper* helper =
      SecurityInterstitialTabHelper::FromWebContents(web_contents());

  // Simulate committing the first interstitial.
  helper->DidFinishNavigation(handle1.get());
  EXPECT_FALSE(blocking_page1_destroyed);
  EXPECT_FALSE(blocking_page2_destroyed);

  // The interstitial at frame 1 moved from pending to commit state.
  EXPECT_TRUE(
      helper->IsInterstitialCommittedForFrame(handle1->GetFrameTreeNodeId()));
  EXPECT_FALSE(
      helper->IsInterstitialCommittedForFrame(handle2->GetFrameTreeNodeId()));
  EXPECT_FALSE(
      helper->IsInterstitialPendingForNavigation(handle1->GetNavigationId()));
  EXPECT_TRUE(
      helper->IsInterstitialPendingForNavigation(handle2->GetNavigationId()));

  // Simulate committing the second interstitial.
  helper->DidFinishNavigation(handle2.get());
  EXPECT_FALSE(blocking_page1_destroyed);
  EXPECT_FALSE(blocking_page2_destroyed);

  // The interstitial at frame 2 moved from pending to commit state.
  EXPECT_TRUE(
      helper->IsInterstitialCommittedForFrame(handle1->GetFrameTreeNodeId()));
  EXPECT_TRUE(
      helper->IsInterstitialCommittedForFrame(handle2->GetFrameTreeNodeId()));
  EXPECT_FALSE(
      helper->IsInterstitialPendingForNavigation(handle1->GetNavigationId()));
  EXPECT_FALSE(
      helper->IsInterstitialPendingForNavigation(handle2->GetNavigationId()));

  helper->FrameDeleted(handle1->GetFrameTreeNodeId());
  EXPECT_TRUE(blocking_page1_destroyed);

  helper->FrameDeleted(handle2->GetFrameTreeNodeId());
  EXPECT_TRUE(blocking_page2_destroyed);
}

// Tests that navigation events associated to one FrameTreeNode do not affect
// the pending interstitial for another FrameTreeNode.
TEST_F(SecurityInterstitialTabHelperTest,
       NavigationForSubframeWithPendingInterstitial) {
  content::RenderFrameHost* subframe1 =
      content::RenderFrameHostTester::For(main_rfh())
          ->AppendChild("subframe 1");
  content::RenderFrameHost* subframe2 =
      content::RenderFrameHostTester::For(main_rfh())
          ->AppendChild("subframe 2");

  std::unique_ptr<content::MockNavigationHandle> committed_handle =
      CreateHandle(subframe1, true, false);
  bool committed_blocking_page_destroyed = false;
  CreateAssociatedBlockingPage(committed_handle.get(),
                               &committed_blocking_page_destroyed);

  std::unique_ptr<content::MockNavigationHandle> pending_handle =
      CreateHandle(subframe2, true, false);
  bool pending_blocking_page_destroyed = false;
  CreateAssociatedBlockingPage(pending_handle.get(),
                               &pending_blocking_page_destroyed);

  // Simulate committing the interstitial for frame 1.
  SecurityInterstitialTabHelper* helper =
      SecurityInterstitialTabHelper::FromWebContents(web_contents());
  helper->DidFinishNavigation(committed_handle.get());
  EXPECT_FALSE(committed_blocking_page_destroyed);
  EXPECT_FALSE(pending_blocking_page_destroyed);

  // The interstitial at frame 1 moved from pending to commit state.
  EXPECT_TRUE(helper->IsInterstitialCommittedForFrame(
      committed_handle->GetFrameTreeNodeId()));
  EXPECT_FALSE(helper->IsInterstitialCommittedForFrame(
      pending_handle->GetFrameTreeNodeId()));
  EXPECT_FALSE(helper->IsInterstitialPendingForNavigation(
      committed_handle->GetNavigationId()));
  EXPECT_TRUE(helper->IsInterstitialPendingForNavigation(
      pending_handle->GetNavigationId()));

  // Simulate a navigation that does not commit.
  std::unique_ptr<content::MockNavigationHandle> non_committed_handle =
      CreateHandle(subframe1, false, false);
  helper->DidFinishNavigation(non_committed_handle.get());

  // The non-committed navigation should not affect the state of committed or
  // pending blocking pages.
  EXPECT_FALSE(committed_blocking_page_destroyed);
  EXPECT_FALSE(pending_blocking_page_destroyed);
  EXPECT_TRUE(helper->IsInterstitialCommittedForFrame(
      committed_handle->GetFrameTreeNodeId()));
  EXPECT_FALSE(helper->IsInterstitialCommittedForFrame(
      pending_handle->GetFrameTreeNodeId()));
  EXPECT_FALSE(helper->IsInterstitialPendingForNavigation(
      committed_handle->GetNavigationId()));
  EXPECT_TRUE(helper->IsInterstitialPendingForNavigation(
      pending_handle->GetNavigationId()));

  // When a navigation does commit, the previous one should be cleaned up.
  std::unique_ptr<content::MockNavigationHandle> next_committed_handle =
      CreateHandle(subframe1, true, false);
  helper->DidFinishNavigation(next_committed_handle.get());
  EXPECT_TRUE(committed_blocking_page_destroyed);
  EXPECT_FALSE(helper->IsInterstitialCommittedForFrame(
      committed_handle->GetFrameTreeNodeId()));
  EXPECT_FALSE(helper->IsInterstitialPendingForNavigation(
      committed_handle->GetNavigationId()));

  // The pending interstitial should still be present.
  EXPECT_FALSE(pending_blocking_page_destroyed);
  EXPECT_TRUE(helper->IsInterstitialPendingForNavigation(
      pending_handle->GetNavigationId()));
  EXPECT_FALSE(helper->IsInterstitialCommittedForFrame(
      pending_handle->GetFrameTreeNodeId()));

  // Frame deletions only affect committed interstitials.
  helper->FrameDeleted(pending_handle->GetFrameTreeNodeId());
  EXPECT_FALSE(pending_blocking_page_destroyed);

  // Delete pending interstitials via web contents destruction.
  DeleteContents();
  EXPECT_TRUE(pending_blocking_page_destroyed);
}

// Tests that navigation events associated to one FrameTreeNode do not affect
// the committed interstitial for another FrameTreeNode.
TEST_F(SecurityInterstitialTabHelperTest,
       NavigationForSubframeWithCommittedInterstitial) {
  content::RenderFrameHost* subframe1 =
      content::RenderFrameHostTester::For(main_rfh())
          ->AppendChild("subframe 1");
  content::RenderFrameHost* subframe2 =
      content::RenderFrameHostTester::For(main_rfh())
          ->AppendChild("subframe 2");

  std::unique_ptr<content::MockNavigationHandle> committed_handle =
      CreateHandle(subframe1, true, false);
  bool committed_blocking_page_destroyed = false;
  CreateAssociatedBlockingPage(committed_handle.get(),
                               &committed_blocking_page_destroyed);

  std::unique_ptr<content::MockNavigationHandle> committed_handle2 =
      CreateHandle(subframe2, true, false);
  bool committed_blocking_page2_destroyed = false;
  CreateAssociatedBlockingPage(committed_handle2.get(),
                               &committed_blocking_page2_destroyed);

  // Simulate committing the interstitial for frame 1.
  SecurityInterstitialTabHelper* helper =
      SecurityInterstitialTabHelper::FromWebContents(web_contents());
  helper->DidFinishNavigation(committed_handle.get());
  EXPECT_FALSE(committed_blocking_page_destroyed);
  EXPECT_FALSE(committed_blocking_page2_destroyed);

  // Simulate committing the interstitial for frame 2.
  helper->DidFinishNavigation(committed_handle2.get());
  EXPECT_FALSE(committed_blocking_page_destroyed);
  EXPECT_FALSE(committed_blocking_page2_destroyed);

  // Both interstitials have moved from pending to commit state.
  EXPECT_TRUE(helper->IsInterstitialCommittedForFrame(
      committed_handle->GetFrameTreeNodeId()));
  EXPECT_TRUE(helper->IsInterstitialCommittedForFrame(
      committed_handle2->GetFrameTreeNodeId()));
  EXPECT_FALSE(helper->IsInterstitialPendingForNavigation(
      committed_handle->GetNavigationId()));
  EXPECT_FALSE(helper->IsInterstitialPendingForNavigation(
      committed_handle2->GetNavigationId()));

  // Simulate a navigation that does not commit.
  std::unique_ptr<content::MockNavigationHandle> non_committed_handle =
      CreateHandle(subframe1, false, false);
  helper->DidFinishNavigation(non_committed_handle.get());

  // The non-committed navigation should not affect the state of committed or
  // pending blocking pages.
  EXPECT_FALSE(committed_blocking_page_destroyed);
  EXPECT_FALSE(committed_blocking_page2_destroyed);
  EXPECT_TRUE(helper->IsInterstitialCommittedForFrame(
      committed_handle->GetFrameTreeNodeId()));
  EXPECT_TRUE(helper->IsInterstitialCommittedForFrame(
      committed_handle2->GetFrameTreeNodeId()));
  EXPECT_FALSE(helper->IsInterstitialPendingForNavigation(
      committed_handle->GetNavigationId()));
  EXPECT_FALSE(helper->IsInterstitialPendingForNavigation(
      committed_handle2->GetNavigationId()));

  // When a navigation does commit, the previous one should be cleaned up.
  std::unique_ptr<content::MockNavigationHandle> next_committed_handle =
      CreateHandle(subframe1, true, false);
  helper->DidFinishNavigation(next_committed_handle.get());
  EXPECT_TRUE(committed_blocking_page_destroyed);
  EXPECT_FALSE(helper->IsInterstitialCommittedForFrame(
      committed_handle->GetFrameTreeNodeId()));
  EXPECT_FALSE(helper->IsInterstitialPendingForNavigation(
      committed_handle->GetNavigationId()));

  // The pending interstitial should still be present.
  EXPECT_FALSE(committed_blocking_page2_destroyed);
  EXPECT_TRUE(helper->IsInterstitialCommittedForFrame(
      committed_handle2->GetFrameTreeNodeId()));

  helper->FrameDeleted(committed_handle2->GetFrameTreeNodeId());
  EXPECT_TRUE(committed_blocking_page2_destroyed);
}

class SecurityInterstitialTabHelperFencedFrameTest
    : public SecurityInterstitialTabHelperTest {
 public:
  SecurityInterstitialTabHelperFencedFrameTest() {
    scoped_feature_list_.InitAndEnableFeatureWithParameters(
        blink::features::kFencedFrames, {{"implementation_type", "mparch"}});
  }
  ~SecurityInterstitialTabHelperFencedFrameTest() override = default;

  content::RenderFrameHost* CreateFencedFrame(
      content::RenderFrameHost* parent) {
    content::RenderFrameHost* fenced_frame =
        content::RenderFrameHostTester::For(parent)->AppendFencedFrame();
    return fenced_frame;
  }

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
};

// Tests that a FencedFrame does not close the interstitial page.
TEST_F(SecurityInterstitialTabHelperFencedFrameTest,
       FencedFrameDoesNotCloseInterstitialPage) {
  std::unique_ptr<content::NavigationHandle> blocking_page_handle =
      CreateHandle(true, false);
  bool blocking_page_destroyed = false;
  CreateAssociatedBlockingPage(blocking_page_handle.get(),
                               &blocking_page_destroyed);
  SecurityInterstitialTabHelper* helper =
      SecurityInterstitialTabHelper::FromWebContents(web_contents());
  helper->DidFinishNavigation(blocking_page_handle.get());
  EXPECT_FALSE(blocking_page_destroyed);
  EXPECT_TRUE(helper->IsDisplayingInterstitial());

  // Navigate a fenced frame and the interstitial page should be kept visible on
  // the fenced frame.
  GURL fenced_frame_url = GURL("https://fencedframe.com");
  content::RenderFrameHostTester::For(main_rfh())
      ->InitializeRenderFrameIfNeeded();
  content::RenderFrameHost* fenced_frame_rfh = CreateFencedFrame(main_rfh());
  std::unique_ptr<content::NavigationSimulator> navigation_simulator =
      content::NavigationSimulator::CreateRendererInitiated(fenced_frame_url,
                                                            fenced_frame_rfh);
  navigation_simulator->Commit();
  fenced_frame_rfh = navigation_simulator->GetFinalRenderFrameHost();
  EXPECT_TRUE(fenced_frame_rfh->IsFencedFrameRoot());
  EXPECT_FALSE(blocking_page_destroyed);
  EXPECT_TRUE(helper->IsDisplayingInterstitial());

  // When a navigation does commit, the fenced frame one should be cleaned up.
  std::unique_ptr<content::NavigationHandle> next_committed_handle =
      CreateHandle(true, false);
  helper->DidFinishNavigation(next_committed_handle.get());
  EXPECT_TRUE(blocking_page_destroyed);
  EXPECT_FALSE(helper->IsDisplayingInterstitial());
}

}  // namespace security_interstitials
