// Copyright 2012 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/content_settings/renderer/content_settings_agent_impl.h"

#include <stddef.h>

#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/run_until.h"
#include "base/test/scoped_feature_list.h"
#include "base/values.h"
#include "components/content_settings/core/common/content_settings.h"
#include "components/content_settings/core/common/content_settings_utils.h"
#include "components/content_settings/core/common/features.h"
#include "components/persistent_cache/pending_backend.h"
#include "content/public/common/url_constants.h"
#include "content/public/renderer/render_frame.h"
#include "content/public/test/render_view_test.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "net/cookies/site_for_cookies.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/associated_interfaces/associated_interface_registry.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/navigation/navigation_params.h"
#include "third_party/blink/public/mojom/loader/code_cache.mojom.h"
#include "third_party/blink/public/platform/browser_interface_broker_proxy.h"
#include "third_party/blink/public/platform/web_url.h"
#include "third_party/blink/public/test/test_web_frame_content_dumper.h"
#include "third_party/blink/public/web/web_view.h"
#include "url/gurl.h"
#include "url/origin.h"
#include "url/url_util.h"

namespace content_settings {
namespace {

constexpr char kAllowlistScheme[] = "foo";

class MockContentSettingsManagerImpl : public mojom::ContentSettingsManager {
 public:
  struct Log {
    int allow_storage_access_count = 0;
    int eager_allow_storage_access_count = 0;
    int on_content_blocked_count = 0;
    ContentSettingsType on_content_blocked_type = ContentSettingsType::DEFAULT;
  };

  explicit MockContentSettingsManagerImpl(Log* log) : log_(log) {}
  ~MockContentSettingsManagerImpl() override = default;

  // mojom::ContentSettingsManager methods:
  void Clone(
      mojo::PendingReceiver<mojom::ContentSettingsManager> receiver) override {
    ADD_FAILURE() << "Not reached";
  }
  void IsStorageAccessAllowed(
      const url::Origin& origin,
      const net::SiteForCookies& site_for_cookies,
      const url::Origin& top_frame_origin,
      base::OnceCallback<void(bool)> callback) override {
    ++log_->eager_allow_storage_access_count;
    std::move(callback).Run(true);
  }
  void AllowStorageAccess(const blink::LocalFrameToken& frame_token,
                          StorageType storage_type,
                          const url::Origin& origin,
                          const net::SiteForCookies& site_for_cookies,
                          const url::Origin& top_frame_origin,
                          base::OnceCallback<void(bool)> callback) override {
    ++log_->allow_storage_access_count;
    std::move(callback).Run(true);
  }
  void OnContentBlocked(const blink::LocalFrameToken& frame_token,
                        ContentSettingsType type) override {
    ++log_->on_content_blocked_count;
    log_->on_content_blocked_type = type;
  }

 private:
  raw_ptr<Log> log_;
};

class MockContentSettingsAgentDelegate
    : public ContentSettingsAgentImpl::Delegate {
 public:
  bool IsSchemeAllowlisted(const std::string& scheme) override {
    return scheme == kAllowlistScheme;
  }
};

class MockContentSettingsAgentImpl : public ContentSettingsAgentImpl {
 public:
  explicit MockContentSettingsAgentImpl(content::RenderFrame* render_frame);

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

  ~MockContentSettingsAgentImpl() override = default;

  const GURL& image_url() const { return image_url_; }
  const std::string& image_origin() const { return image_origin_; }

  // ContentSettingAgentImpl methods:
  void BindContentSettingsManager(
      mojo::Remote<mojom::ContentSettingsManager>* manager) override;

  int allow_storage_access_count() const {
    return log_.allow_storage_access_count;
  }
  int eager_allow_storage_access_count() const {
    return log_.eager_allow_storage_access_count;
  }
  int on_content_blocked_count() const { return log_.on_content_blocked_count; }
  ContentSettingsType on_content_blocked_type() const {
    return log_.on_content_blocked_type;
  }

 private:
  MockContentSettingsManagerImpl::Log log_;
  const GURL image_url_;
  const std::string image_origin_;
};

MockContentSettingsAgentImpl::MockContentSettingsAgentImpl(
    content::RenderFrame* render_frame)
    : ContentSettingsAgentImpl(
          render_frame,
          std::make_unique<MockContentSettingsAgentDelegate>()),
      image_url_("http://www.foo.com/image.jpg"),
      image_origin_("http://www.foo.com") {}

void MockContentSettingsAgentImpl::BindContentSettingsManager(
    mojo::Remote<mojom::ContentSettingsManager>* manager) {
  mojo::MakeSelfOwnedReceiver(
      std::make_unique<MockContentSettingsManagerImpl>(&log_),
      manager->BindNewPipeAndPassReceiver());
}

// Evaluates a boolean `predicate` every time a provisional load is committed in
// the given `frame` while the instance of this class is in scope, and verifies
// that the result matches the `expectation`.
class CommitTimeConditionChecker : public content::RenderFrameObserver {
 public:
  using Predicate = base::RepeatingCallback<bool()>;

  CommitTimeConditionChecker(content::RenderFrame* frame,
                             const Predicate& predicate,
                             bool expectation)
      : content::RenderFrameObserver(frame),
        predicate_(predicate),
        expectation_(expectation) {}

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

 protected:
  // RenderFrameObserver:
  void OnDestruct() override {}
  void DidCommitProvisionalLoad(ui::PageTransition transition) override {
    EXPECT_EQ(expectation_, predicate_.Run());
  }

 private:
  const Predicate predicate_;
  const bool expectation_;
};

}  // namespace

enum class BackgroundResourceFetchTestCase {
  kBackgroundResourceFetchEnabled,
  kBackgroundResourceFetchDisabled,
};

class ContentSettingsAgentImplBrowserTest
    : public content::RenderViewTest,
      public testing::WithParamInterface<BackgroundResourceFetchTestCase> {
 protected:
  void SetUp() override {
    std::vector<base::test::FeatureRef> enabled_features;
    std::vector<base::test::FeatureRef> disabled_features;
    if (IsBackgroundResourceFetchEnabled()) {
      enabled_features.push_back(blink::features::kBackgroundResourceFetch);
    } else {
      disabled_features.push_back(blink::features::kBackgroundResourceFetch);
    }
    feature_background_resource_fetch_.InitWithFeatures(enabled_features,
                                                        disabled_features);
    RenderViewTest::SetUp();

    // Set up a fake url loader factory to ensure that script loader can create
    // a URLLoader.
    CreateFakeURLLoaderFactory();

    // Unbind the ContentSettingsAgent interface that would be registered by
    // the ContentSettingsAgentImpl created when the render frame is created.
    GetMainRenderFrame()->GetAssociatedInterfaceRegistry()->RemoveInterface(
        mojom::ContentSettingsAgent::Name_);

    // Bind a FakeCodeCacheHost which handles FetchCachedCode() method, because
    // script loading is blocked until the callback of FetchCachedCode() is
    // called.
    GetMainRenderFrame()->GetBrowserInterfaceBroker().SetBinderForTesting(
        blink::mojom::CodeCacheHost::Name_,
        base::BindRepeating(
            &ContentSettingsAgentImplBrowserTest::OnCodeCacheHostRequest,
            base::Unretained(this)));
  }

 private:
  class FakeCodeCacheHost : public blink::mojom::CodeCacheHost {
   public:
    explicit FakeCodeCacheHost(
        mojo::PendingReceiver<blink::mojom::CodeCacheHost> receiver) {
      receiver_.Bind(std::move(receiver));
    }

    void GetPendingBackend(blink::mojom::CodeCacheType cache_type,
                           GetPendingBackendCallback callback) override {
      std::move(callback).Run(std::nullopt);
    }
    void DidGenerateCacheableMetadata(blink::mojom::CodeCacheType cache_type,
                                      const GURL& url,
                                      base::Time expected_response_time,
                                      mojo_base::BigBuffer data) override {}
    void DidGenerateSourceKeyedCacheableMetadata(
        const std::vector<uint8_t>& source_hash,
        mojo_base::BigBuffer data) override {}
    void FetchCachedCode(blink::mojom::CodeCacheType cache_type,
                         const GURL& url,
                         FetchCachedCodeCallback callback) override {
      std::move(callback).Run(base::Time(), {});
    }
    void ClearCodeCacheEntry(blink::mojom::CodeCacheType cache_type,
                             const GURL& url) override {}
    void DidGenerateCacheableMetadataInCacheStorage(
        const GURL& url,
        base::Time expected_response_time,
        mojo_base::BigBuffer data,
        const std::string& cache_storage_cache_name) override {}

   private:
    mojo::Receiver<blink::mojom::CodeCacheHost> receiver_{this};
  };

  bool IsBackgroundResourceFetchEnabled() const {
    return GetParam() ==
           BackgroundResourceFetchTestCase::kBackgroundResourceFetchEnabled;
  }

  void OnCodeCacheHostRequest(mojo::ScopedMessagePipeHandle handle) {
    fake_code_cache_hosts_.emplace_back(std::make_unique<FakeCodeCacheHost>(
        mojo::PendingReceiver<blink::mojom::CodeCacheHost>(std::move(handle))));
  }

  std::vector<std::unique_ptr<FakeCodeCacheHost>> fake_code_cache_hosts_;
  base::test::ScopedFeatureList feature_background_resource_fetch_;
};

INSTANTIATE_TEST_SUITE_P(
    All,
    ContentSettingsAgentImplBrowserTest,
    testing::ValuesIn(
        {BackgroundResourceFetchTestCase::kBackgroundResourceFetchEnabled,
         BackgroundResourceFetchTestCase::kBackgroundResourceFetchDisabled}),
    [](const testing::TestParamInfo<BackgroundResourceFetchTestCase>& info) {
      switch (info.param) {
        case (BackgroundResourceFetchTestCase::kBackgroundResourceFetchEnabled):
          return "BackgroundResourceFetchEnabled";
        case (
            BackgroundResourceFetchTestCase::kBackgroundResourceFetchDisabled):
          return "BackgroundResourceFetchDisabled";
      }
    });

TEST_P(ContentSettingsAgentImplBrowserTest, DidBlockContentType) {
  MockContentSettingsAgentImpl mock_agent(GetMainRenderFrame());
  mock_agent.DidBlockContentType(ContentSettingsType::COOKIES);
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(1, mock_agent.on_content_blocked_count());
  EXPECT_EQ(ContentSettingsType::COOKIES, mock_agent.on_content_blocked_type());

  // Blocking the same content type a second time shouldn't send a notification.
  mock_agent.DidBlockContentType(ContentSettingsType::COOKIES);
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(1, mock_agent.on_content_blocked_count());
}

// Tests that multiple invocations of AllowStorageAccessSync result in a single
// IPC.
TEST_P(ContentSettingsAgentImplBrowserTest, AllowStorageAccessSync) {
  base::HistogramTester histogram_tester;
  // Load some HTML, so we have a valid security origin.
  LoadHTMLWithUrlOverride("<html></html>", "https://example.com/");
  MockContentSettingsAgentImpl mock_agent(GetMainRenderFrame());
  mock_agent.AllowStorageAccessSync(
      blink::WebContentSettingsClient::StorageType::kLocalStorage);
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(1, mock_agent.allow_storage_access_count());
  histogram_tester.ExpectTotalCount("ContentSettings.AllowStorageAccessSync",
                                    1);
  histogram_tester.ExpectBucketCount(
      "ContentSettings.AllowStorageAccessSync.CacheHit", false, 1);

  // Accessing localStorage from the same origin again shouldn't result in a
  // new IPC.
  mock_agent.AllowStorageAccessSync(
      blink::WebContentSettingsClient::StorageType::kLocalStorage);
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(1, mock_agent.allow_storage_access_count());
  histogram_tester.ExpectTotalCount("ContentSettings.AllowStorageAccessSync",
                                    2);
  histogram_tester.ExpectBucketCount(
      "ContentSettings.AllowStorageAccessSync.CacheHit", true, 1);
}

// Tests that multiple invocations of AllowStorageAccess result in a single IPC.
TEST_P(ContentSettingsAgentImplBrowserTest, AllowStorageAccess) {
  // Load some HTML, so we have a valid security origin.
  LoadHTMLWithUrlOverride("<html></html>", "https://example.com/");
  MockContentSettingsAgentImpl mock_agent(GetMainRenderFrame());
  mock_agent.AllowStorageAccess(
      blink::WebContentSettingsClient::StorageType::kLocalStorage,
      base::DoNothing());
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(1, mock_agent.allow_storage_access_count());

  // Accessing localStorage from the same origin again shouldn't result in a
  // new IPC.
  mock_agent.AllowStorageAccess(
      blink::WebContentSettingsClient::StorageType::kLocalStorage,
      base::DoNothing());
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(1, mock_agent.allow_storage_access_count());
}

TEST_P(ContentSettingsAgentImplBrowserTest, MixedAutoupgradesDisabledByRules) {
  MockContentSettingsAgentImpl mock_agent(GetMainRenderFrame());

  LoadHTMLWithUrlOverride("<html></html>", "https://example.com/");

  // Set the default mixed content blocking setting.
  RendererContentSettingRules content_setting_rules;
  ContentSettingsForOneType& mixed_content_setting_rules =
      content_setting_rules.mixed_content_rules;
  mixed_content_setting_rules.push_back(ContentSettingPatternSource(
      ContentSettingsPattern::Wildcard(), ContentSettingsPattern::Wildcard(),
      ContentSettingToValue(CONTENT_SETTING_BLOCK), ProviderType::kNone,
      false));

  ContentSettingsAgentImpl* agent =
      ContentSettingsAgentImpl::Get(GetMainRenderFrame());
  agent->SetRendererContentSettingRulesForTest(content_setting_rules);
  EXPECT_TRUE(agent->ShouldAutoupgradeMixedContent());

  // Create an exception which allows mixed content.
  mixed_content_setting_rules.insert(
      mixed_content_setting_rules.begin(),
      ContentSettingPatternSource(
          ContentSettingsPattern::FromString("https://example.com/"),
          ContentSettingsPattern::Wildcard(),
          ContentSettingToValue(CONTENT_SETTING_ALLOW), ProviderType::kNone,
          false));
  agent->SetRendererContentSettingRulesForTest(content_setting_rules);

  EXPECT_FALSE(agent->ShouldAutoupgradeMixedContent());
}

TEST_P(ContentSettingsAgentImplBrowserTest, MixedAutoupgradesNoSettingsSet) {
  MockContentSettingsAgentImpl mock_agent(GetMainRenderFrame());

  ContentSettingsAgentImpl* agent =
      ContentSettingsAgentImpl::Get(GetMainRenderFrame());
  EXPECT_TRUE(agent->ShouldAutoupgradeMixedContent());
}

TEST_P(ContentSettingsAgentImplBrowserTest,
       EagerStorageAccessPermissionCheckEnabled) {
  base::test::ScopedFeatureList feature_list;
  feature_list.InitAndEnableFeature(
      features::kEagerStorageAccessPermissionCheck);

  MockContentSettingsAgentImpl mock_agent(GetMainRenderFrame());

  // Load some HTML. This will trigger DidCommitProvisionalLoad.
  LoadHTMLWithUrlOverride("<html></html>", "https://example.com/");

  // Wait for the eager IPC.
  EXPECT_TRUE(base::test::RunUntil(
      [&]() { return mock_agent.eager_allow_storage_access_count() == 2; }));

  // Subsequent sync access should hit the cache and NOT trigger more IPCs
  // directly, but WILL trigger one async IPC for UI metric logging (which has
  // enable_logging_usage=true).
  mock_agent.AllowStorageAccessSync(
      blink::WebContentSettingsClient::StorageType::kLocalStorage);

  // Wait for the UI logging async IPC to be sent.
  EXPECT_TRUE(base::test::RunUntil(
      [&]() { return mock_agent.allow_storage_access_count() == 1; }));
}

TEST_P(ContentSettingsAgentImplBrowserTest,
       EagerStorageAccessPermissionCheckDisabled) {
  base::test::ScopedFeatureList feature_list;
  feature_list.InitAndDisableFeature(
      features::kEagerStorageAccessPermissionCheck);

  MockContentSettingsAgentImpl mock_agent(GetMainRenderFrame());

  // Load some HTML. This will trigger DidCommitProvisionalLoad.
  LoadHTMLWithUrlOverride("<html></html>", "https://example.com/");

  // Verify no eager IPCs were sent.
  base::RunLoop run_loop;
  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, run_loop.QuitClosure());
  run_loop.Run();

  EXPECT_EQ(0, mock_agent.eager_allow_storage_access_count());

  // Sync access should trigger IPC.
  mock_agent.AllowStorageAccessSync(
      blink::WebContentSettingsClient::StorageType::kLocalStorage);

  // Wait for the sync IPC to be processed.
  EXPECT_TRUE(base::test::RunUntil(
      [&]() { return mock_agent.allow_storage_access_count() == 1; }));
}

}  // namespace content_settings
