// 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/extensions/extension_tab_util.h"

#include "base/json/json_reader.h"
#include "base/strings/stringprintf.h"
#include "base/test/gmock_expected_support.h"
#include "base/values.h"
#include "chrome/browser/extensions/extension_service_test_base.h"
#include "chrome/browser/extensions/extension_util.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
#include "components/sessions/content/session_tab_helper.h"
#include "components/sync_preferences/testing_pref_service_syncable.h"
#include "content/public/test/web_contents_tester.h"
#include "extensions/browser/extension_registrar.h"
#include "extensions/browser/pref_names.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_builder.h"
#include "extensions/common/mojom/context_type.mojom.h"
#include "extensions/common/permissions/permission_set.h"
#include "extensions/common/permissions/permissions_data.h"
#include "extensions/common/url_pattern_set.h"
#include "testing/gtest/include/gtest/gtest.h"

static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));

namespace extensions {

TEST(ExtensionTabUtilTest, ScrubTabBehaviorForTabsPermission) {
  auto extension = ExtensionBuilder("Extension with tabs permission")
                       .AddAPIPermission("tabs")
                       .Build();
  ExtensionTabUtil::ScrubTabBehavior scrub_tab_behavior =
      ExtensionTabUtil::GetScrubTabBehavior(extension.get(),
                                            mojom::ContextType::kUnspecified,
                                            GURL("http://www.google.com"));
  EXPECT_EQ(ExtensionTabUtil::kDontScrubTab, scrub_tab_behavior.committed_info);
  EXPECT_EQ(ExtensionTabUtil::kDontScrubTab, scrub_tab_behavior.pending_info);
}

TEST(ExtensionTabUtilTest, ScrubTabBehaviorForNoPermission) {
  auto extension = ExtensionBuilder("Extension with no permissions").Build();
  ExtensionTabUtil::ScrubTabBehavior scrub_tab_behavior =
      ExtensionTabUtil::GetScrubTabBehavior(extension.get(),
                                            mojom::ContextType::kUnspecified,
                                            GURL("http://www.google.com"));
  EXPECT_EQ(ExtensionTabUtil::kScrubTabFully,
            scrub_tab_behavior.committed_info);
  EXPECT_EQ(ExtensionTabUtil::kScrubTabFully, scrub_tab_behavior.pending_info);
}

TEST(ExtensionTabUtilTest, ScrubTabBehaviorForHostPermission) {
  auto extension = ExtensionBuilder("Extension with host permission")
                       .AddHostPermission("*://www.google.com/*")
                       .Build();
  ExtensionTabUtil::ScrubTabBehavior scrub_tab_behavior =
      ExtensionTabUtil::GetScrubTabBehavior(
          extension.get(), mojom::ContextType::kUnspecified,
          GURL("http://www.google.com/some/path"));
  EXPECT_EQ(ExtensionTabUtil::kDontScrubTab, scrub_tab_behavior.committed_info);
  EXPECT_EQ(ExtensionTabUtil::kDontScrubTab, scrub_tab_behavior.pending_info);
}

TEST(ExtensionTabUtilTest, ScrubTabBehaviorForExtensionOwnOrigin_GURL) {
  auto extension = ExtensionBuilder("Extension with activeTab permission")
                       .AddAPIPermission("activeTab")
                       .Build();
  GURL options_url = extension->GetResourceURL("options.html");
  ExtensionTabUtil::ScrubTabBehavior scrub_tab_behavior =
      ExtensionTabUtil::GetScrubTabBehavior(
          extension.get(), mojom::ContextType::kPrivilegedExtension,
          options_url);
  EXPECT_EQ(ExtensionTabUtil::kDontScrubTab, scrub_tab_behavior.committed_info);
  EXPECT_EQ(ExtensionTabUtil::kDontScrubTab, scrub_tab_behavior.pending_info);
}

TEST(ExtensionTabUtilTest, ScrubTabBehaviorForNoExtension) {
  ExtensionTabUtil::ScrubTabBehavior scrub_tab_behavior =
      ExtensionTabUtil::GetScrubTabBehavior(nullptr,
                                            mojom::ContextType::kUnspecified,
                                            GURL("http://www.google.com"));
  EXPECT_EQ(ExtensionTabUtil::kScrubTabFully,
            scrub_tab_behavior.committed_info);
  EXPECT_EQ(ExtensionTabUtil::kScrubTabFully, scrub_tab_behavior.pending_info);
}

TEST(ExtensionTabUtilTest, ScrubTabBehaviorForWebUI) {
  ExtensionTabUtil::ScrubTabBehavior scrub_tab_behavior =
      ExtensionTabUtil::GetScrubTabBehavior(nullptr, mojom::ContextType::kWebUi,
                                            GURL("http://www.google.com"));
  EXPECT_EQ(ExtensionTabUtil::kDontScrubTab, scrub_tab_behavior.committed_info);
  EXPECT_EQ(ExtensionTabUtil::kDontScrubTab, scrub_tab_behavior.pending_info);
}

TEST(ExtensionTabUtilTest, ScrubTabBehaviorForWebUIUntrusted) {
  ExtensionTabUtil::ScrubTabBehavior scrub_tab_behavior =
      ExtensionTabUtil::GetScrubTabBehavior(nullptr,
                                            mojom::ContextType::kUntrustedWebUi,
                                            GURL("http://www.google.com"));
  EXPECT_EQ(ExtensionTabUtil::kScrubTabFully,
            scrub_tab_behavior.committed_info);
  EXPECT_EQ(ExtensionTabUtil::kScrubTabFully, scrub_tab_behavior.pending_info);
}

TEST(ExtensionTabUtilTest, ResolvePossiblyRelativeURL) {
  auto extension = ExtensionBuilder("test").Build();
  EXPECT_EQ(ExtensionTabUtil::ResolvePossiblyRelativeURL(
                "http://example.com/path", extension.get()),
            GURL("http://example.com/path"));
  EXPECT_EQ(
      ExtensionTabUtil::ResolvePossiblyRelativeURL("path", extension.get()),
      GURL("chrome-extension://jpignaibiiemhngfjkcpokkamffknabf/path"));
  EXPECT_EQ(ExtensionTabUtil::ResolvePossiblyRelativeURL("path", nullptr),
            GURL("path"));
}

class ChromeExtensionNavigationTest : public ExtensionServiceTestBase {
 public:
  ChromeExtensionNavigationTest() = default;

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

  void SetUp() override;
};

void ChromeExtensionNavigationTest::SetUp() {
  ExtensionServiceTestBase::SetUp();
  InitializeExtensionServiceWithUpdater();
}

TEST_F(ChromeExtensionNavigationTest, PrepareURLForNavigation) {
  auto extension = ExtensionBuilder("test").Build();
  // A fully qualified URL should return the same URL.
  {
    const std::string kTestUrl("http://google.com");
    auto url = ExtensionTabUtil::PrepareURLForNavigation(
        kTestUrl, extension.get(), browser_context());
    EXPECT_THAT(url, base::test::ValueIs(GURL(kTestUrl)));
  }
  // A relative path should return a URL relative to the extension's base URL.
  {
    const std::string kTestPath("foo");
    auto url = ExtensionTabUtil::PrepareURLForNavigation(
        kTestPath, extension.get(), browser_context());
    EXPECT_THAT(url, base::test::ValueIs(extension->GetResourceURL(kTestPath)));
  }
  // A kill URL should return false and set the error. There are several
  // different potential kill URLs and this just checks one of them.
  {
    const std::string kKillURL("chrome://crash");
    auto url = ExtensionTabUtil::PrepareURLForNavigation(
        kKillURL, extension.get(), browser_context());
    EXPECT_THAT(url,
                base::test::ErrorIs(ExtensionTabUtil::kNoCrashBrowserError));
  }
  // Hang URLs and other similar debug urls should also return false and set the
  // error.
  {
    const std::string kHangURL("chrome://hang");
    auto url = ExtensionTabUtil::PrepareURLForNavigation(
        kHangURL, extension.get(), browser_context());
    ASSERT_FALSE(url.has_value());
    EXPECT_EQ(ExtensionTabUtil::kNoCrashBrowserError, url.error());
  }
  // JavaScript URLs should return false and set the error.
  {
    const std::string kJavaScriptURL("javascript:alert('foo');");
    auto url = ExtensionTabUtil::PrepareURLForNavigation(
        kJavaScriptURL, extension.get(), browser_context());
    ASSERT_FALSE(url.has_value());
    EXPECT_EQ(ExtensionTabUtil::kJavaScriptUrlsNotAllowedInExtensionNavigations,
              url.error());
  }
  // File URLs should return false and set the error.
  {
    const std::string kFileURL("file:///etc/passwd");
    auto url = ExtensionTabUtil::PrepareURLForNavigation(
        kFileURL, extension.get(), browser_context());
    ASSERT_FALSE(url.has_value());
    EXPECT_EQ(ExtensionTabUtil::kFileUrlsNotAllowedInExtensionNavigations,
              url.error());
  }
  // File URLs with view-source scheme should return false and set the error.
  {
    const std::string kViewSourceFileURL("view-source:file:///etc/passwd");
    auto url = ExtensionTabUtil::PrepareURLForNavigation(
        kViewSourceFileURL, extension.get(), browser_context());
    ASSERT_FALSE(url.has_value());
    EXPECT_EQ(ExtensionTabUtil::kFileUrlsNotAllowedInExtensionNavigations,
              url.error());
  }
  // File URLs are returned when the extension has access to file.
  {
    util::SetAllowFileAccess(extension->id(), browser_context(), true);
    const std::string kFileURLWithAccess("file:///etc/passwd");
    auto url = ExtensionTabUtil::PrepareURLForNavigation(
        kFileURLWithAccess, extension.get(), browser_context());
    EXPECT_THAT(url, base::test::ValueIs(GURL(kFileURLWithAccess)));
  }
  // Regression test for crbug.com/40073743. Ensure that file URLs are returned
  // when the call originates from non-extension contexts (e.g. WebUI contexts).
  {
    const std::string kFileURL("file:///etc/passwd");
    auto url = ExtensionTabUtil::PrepareURLForNavigation(
        kFileURL, /*extension=*/nullptr, browser_context());
    EXPECT_THAT(url, base::test::ValueIs(GURL(kFileURL)));
  }
  // Regression test for crbug.com/348405962.
  {
    const std::string kTestPath("mailto:8080?cc=&bcc=&subject=&body=");
    auto url = ExtensionTabUtil::PrepareURLForNavigation(
        kTestPath, extension.get(), browser_context());
    EXPECT_THAT(url, base::test::ValueIs(GURL(kTestPath)));
  }
}

// Regression test for https://crbug.com/496615345. Tests that when an extension
// has a tab-specific permission (e.g. activeTab) granted for a specific origin,
// GetScrubTabBehavior scrubs cross-origin URLs on that tab even if the tab's
// temporary permissions have not yet been revoked.
TEST_F(ChromeExtensionNavigationTest,
       ScrubTabBehaviorForTabSpecificPermissionOriginScoped) {
  auto extension = ExtensionBuilder("Extension with activeTab permission")
                       .AddAPIPermission("activeTab")
                       .Build();
  std::unique_ptr<content::WebContents> web_contents(
      content::WebContentsTester::CreateTestWebContents(profile(), nullptr));
  sessions::SessionTabHelper::CreateForWebContents(web_contents.get(),
                                                   base::NullCallback());
  content::WebContentsTester::For(web_contents.get())
      ->NavigateAndCommit(GURL("http://www.google.com/some/path"));
  // Simulate the extension having activeTab on this tab by directly populating
  // tab-specific permissions in PermissionsData (avoiding async CORS and IPC
  // side effects from ActiveTabPermissionGranter in a unittest).
  const int kTabId = ExtensionTabUtil::GetTabId(web_contents.get());
  APIPermissionSet apis;
  apis.insert(mojom::APIPermissionID::kTab);
  URLPatternSet hosts;
  hosts.AddOrigin(URLPattern::SCHEME_ALL, GURL("http://www.google.com"));
  PermissionSet tab_permissions(std::move(apis), ManifestPermissionSet(),
                                hosts.Clone(), hosts.Clone());
  extension->permissions_data()->UpdateTabSpecificPermissions(kTabId,
                                                              tab_permissions);

  ExtensionTabUtil::ScrubTabBehavior scrub_tab_behavior =
      ExtensionTabUtil::GetScrubTabBehavior(extension.get(),
                                            mojom::ContextType::kUnspecified,
                                            web_contents.get());
  EXPECT_EQ(ExtensionTabUtil::kDontScrubTab, scrub_tab_behavior.committed_info);

  content::WebContentsTester::For(web_contents.get())
      ->NavigateAndCommit(GURL("http://www.evil.com/some/path"));
  scrub_tab_behavior = ExtensionTabUtil::GetScrubTabBehavior(
      extension.get(), mojom::ContextType::kUnspecified, web_contents.get());
  EXPECT_EQ(ExtensionTabUtil::kScrubTabFully,
            scrub_tab_behavior.committed_info);
}

// Tests that an extension without global "tabs" or explicit host permissions
// (such as an extension with only activeTab) does NOT have its own origin URLs
// scrubbed when navigating or querying tabs.
TEST_F(ChromeExtensionNavigationTest,
       ScrubTabBehaviorForExtensionOwnOrigin_WebContents) {
  auto extension = ExtensionBuilder("Extension without permissions").Build();
  ExtensionRegistrar::Get(profile())->AddExtension(extension);

  std::unique_ptr<content::WebContents> web_contents(
      content::WebContentsTester::CreateTestWebContents(profile(), nullptr));
  sessions::SessionTabHelper::CreateForWebContents(web_contents.get(),
                                                   base::NullCallback());

  GURL options_url = extension->GetResourceURL("options.html");
  content::WebContentsTester::For(web_contents.get())
      ->NavigateAndCommit(options_url);

  ExtensionTabUtil::ScrubTabBehavior scrub_tab_behavior =
      ExtensionTabUtil::GetScrubTabBehavior(
          extension.get(), mojom::ContextType::kPrivilegedExtension,
          web_contents.get());
  EXPECT_EQ(ExtensionTabUtil::kDontScrubTab, scrub_tab_behavior.committed_info);

  // Also verify with GURL overload.
  scrub_tab_behavior = ExtensionTabUtil::GetScrubTabBehavior(
      extension.get(), mojom::ContextType::kPrivilegedExtension, options_url);
  EXPECT_EQ(ExtensionTabUtil::kDontScrubTab, scrub_tab_behavior.committed_info);
  EXPECT_EQ(ExtensionTabUtil::kDontScrubTab, scrub_tab_behavior.pending_info);
}

TEST_F(ChromeExtensionNavigationTest,
       PrepareURLForNavigationWithEnterprisePolicy) {
  // Set the extension to allow file URL navigation via enterprise policy.
  std::string extension_id = "abcdefghijklmnopabcdefghijklmnop";
  std::string json = base::StringPrintf(
      R"({
        "%s": {
          "file_url_navigation_allowed": true
        }
      })",
      extension_id.c_str());

  std::optional<base::Value> settings =
      base::JSONReader::Read(json, base::JSON_PARSE_CHROMIUM_EXTENSIONS);
  testing_pref_service()->SetManagedPref(
      pref_names::kExtensionManagement,
      base::Value::ToUniquePtrValue(std::move(settings.value())));

  auto extension = ExtensionBuilder("test").SetID(extension_id).Build();

  // File URLs are returned when the extension has access to file.
  const std::string kFileURLWithEnterprisePolicy("file:///etc/passwd");
  auto url = ExtensionTabUtil::PrepareURLForNavigation(
      kFileURLWithEnterprisePolicy, extension.get(), browser_context());
  EXPECT_THAT(url, base::test::ValueIs(GURL(kFileURLWithEnterprisePolicy)));
}

TEST_F(ChromeExtensionNavigationTest, PrepareURLForNavigationWithPDFViewer) {
  // Set ID for PDF viewer extension.
  auto extension =
      ExtensionBuilder("test").SetID(extension_misc::kPdfExtensionId).Build();

  // File URLs are returned when the extension has access to file.
  const std::string kFileURLWithPDFViewer("file:///etc/passwd");
  auto url = ExtensionTabUtil::PrepareURLForNavigation(
      kFileURLWithPDFViewer, extension.get(), browser_context());
  EXPECT_THAT(url, base::test::ValueIs(GURL(kFileURLWithPDFViewer)));
}

TEST_F(ChromeExtensionNavigationTest, PrepareURLForNavigationOnDevtools) {
  const std::string kDevtoolsURL(
      "devtools://devtools/bundled/devtools_app.html");
  // A devtools url should return false and set the error.
  {
    auto no_permission_extension = ExtensionBuilder("none").Build();
    auto url = ExtensionTabUtil::PrepareURLForNavigation(
        kDevtoolsURL, no_permission_extension.get(), browser_context());
    EXPECT_THAT(
        url, base::test::ErrorIs(ExtensionTabUtil::kCannotNavigateToDevtools));
  }
}

TEST_F(ChromeExtensionNavigationTest,
       PrepareURLForNavigationOnChromeUntrusted) {
  const std::string kChromeUntrustedURL("chrome-untrusted://terminal/");
  auto extension = ExtensionBuilder("none").Build();
  auto url = ExtensionTabUtil::PrepareURLForNavigation(
      kChromeUntrustedURL, extension.get(), browser_context());
  EXPECT_THAT(url, base::test::ErrorIs(
                       ExtensionTabUtil::kCannotNavigateToChromeUntrusted));
}

}  // namespace extensions
