// Copyright 2023 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_util.h"

#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/strings/strcat.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/gtest_util.h"
#include "base/test/scoped_feature_list.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/extensions/chrome_test_extension_loader.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/extension_service_test_base.h"
#include "chrome/browser/extensions/external_provider_impl.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/chrome_constants.h"
#include "chrome/test/base/testing_profile.h"
#include "chrome/test/base/testing_profile_manager.h"
#include "components/policy/core/common/mock_configuration_policy_provider.h"
#include "components/policy/core/common/policy_service_impl.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/disable_reason.h"
#include "extensions/browser/extension_registrar.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_util.h"
#include "extensions/browser/pref_names.h"
#include "extensions/browser/test_extension_registry_observer.h"
#include "extensions/browser/ui_util.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/extension_builder.h"
#include "extensions/common/extension_features.h"
#include "extensions/common/extension_urls.h"
#include "extensions/common/mojom/manifest.mojom-shared.h"
#include "extensions/common/permissions/permissions_data.h"
#include "extensions/test/test_extension_dir.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "url/gurl.h"

static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));

namespace extensions {

namespace {

#if BUILDFLAG(IS_CHROMEOS)
constexpr char kExtensionUpdateUrl[] =
    "https://clients2.google.com/service/update2/crx";  // URL of Chrome Web
                                                        // Store backend.
#endif

}  // namespace

class ExtensionUtilUnittest : public ExtensionServiceTestBase {
 public:
  void SetUp() override {
    ExtensionServiceTestBase::SetUp();
    InitializeEmptyExtensionService();
  }
};

TEST_F(ExtensionUtilUnittest, SetAllowFileAccess) {
  constexpr char kManifest[] =
      R"({
           "name": "foo",
           "version": "1.0",
           "manifest_version": 3,
           "host_permissions": ["<all_urls>"]
         })";

  TestExtensionDir dir;
  dir.WriteManifest(kManifest);

  ChromeTestExtensionLoader loader(profile());
  // An unpacked extension would get file access by default, so disabled it on
  // the loader.
  loader.set_allow_file_access(false);

  scoped_refptr<const Extension> extension =
      loader.LoadExtension(dir.UnpackedPath());
  const std::string extension_id = extension->id();

  GURL file_url("file://etc");
  std::unique_ptr<content::WebContents> web_contents(
      content::WebContentsTester::CreateTestWebContents(profile(), nullptr));
  int tab_id = sessions::SessionTabHelper::IdForTab(web_contents.get()).id();

  // Initially the file access pref will be false and the extension will not be
  // able to capture a file URL page.
  EXPECT_FALSE(util::AllowFileAccess(extension_id, profile()));
  EXPECT_FALSE(extension->permissions_data()->CanCaptureVisiblePage(
      file_url, tab_id, nullptr, CaptureRequirement::kActiveTabOrAllUrls));

  // Calling SetAllowFileAccess should reload the extension with file access.
  {
    TestExtensionRegistryObserver observer(registry(), extension_id);
    util::SetAllowFileAccess(extension_id, browser_context(), true);
    extension = observer.WaitForExtensionInstalled();
  }

  EXPECT_TRUE(util::AllowFileAccess(extension_id, profile()));
  EXPECT_TRUE(extension->permissions_data()->CanCaptureVisiblePage(
      file_url, tab_id, nullptr, CaptureRequirement::kActiveTabOrAllUrls));

  // Removing the file access should reload the extension again back to not
  // having file access.
  {
    TestExtensionRegistryObserver observer(registry(), extension_id);
    util::SetAllowFileAccess(extension_id, browser_context(), false);
    extension = observer.WaitForExtensionInstalled();
  }

  EXPECT_FALSE(util::AllowFileAccess(extension_id, profile()));
  EXPECT_FALSE(extension->permissions_data()->CanCaptureVisiblePage(
      file_url, tab_id, nullptr, CaptureRequirement::kActiveTabOrAllUrls));
}

TEST_F(ExtensionUtilUnittest, SetAllowFileAccessWhileDisabled) {
  constexpr char kManifest[] =
      R"({
           "name": "foo",
           "version": "1.0",
           "manifest_version": 3,
           "host_permissions": ["<all_urls>"]
         })";

  TestExtensionDir dir;
  dir.WriteManifest(kManifest);

  ChromeTestExtensionLoader loader(profile());
  // An unpacked extension would get file access by default, so disabled it on
  // the loader.
  loader.set_allow_file_access(false);

  scoped_refptr<const Extension> extension =
      loader.LoadExtension(dir.UnpackedPath());
  const std::string extension_id = extension->id();

  GURL file_url("file://etc");
  std::unique_ptr<content::WebContents> web_contents(
      content::WebContentsTester::CreateTestWebContents(profile(), nullptr));
  int tab_id = sessions::SessionTabHelper::IdForTab(web_contents.get()).id();

  // Initially the file access pref will be false and the extension will not be
  // able to capture a file URL page.
  EXPECT_FALSE(util::AllowFileAccess(extension_id, profile()));
  EXPECT_FALSE(extension->permissions_data()->CanCaptureVisiblePage(
      file_url, tab_id, nullptr, CaptureRequirement::kActiveTabOrAllUrls));

  // Disabling the extension then calling SetAllowFileAccess should reload the
  // extension with file access.
  registrar()->DisableExtension(extension_id,
                                {disable_reason::DISABLE_USER_ACTION});
  {
    TestExtensionRegistryObserver observer(registry(), extension_id);
    util::SetAllowFileAccess(extension_id, browser_context(), true);
    extension = observer.WaitForExtensionInstalled();
  }
  // The extension should still be disabled.
  EXPECT_FALSE(registrar()->IsExtensionEnabled(extension_id));

  registrar()->EnableExtension(extension_id);
  EXPECT_TRUE(util::AllowFileAccess(extension_id, profile()));
  EXPECT_TRUE(extension->permissions_data()->CanCaptureVisiblePage(
      file_url, tab_id, nullptr, CaptureRequirement::kActiveTabOrAllUrls));

  // Disabling the extension and then removing the file access should reload it
  // again back to not having file access. Regression test for
  // crbug.com/40061772.
  registrar()->DisableExtension(extension_id,
                                {disable_reason::DISABLE_USER_ACTION});
  {
    TestExtensionRegistryObserver observer(registry(), extension_id);
    util::SetAllowFileAccess(extension_id, browser_context(), false);
    extension = observer.WaitForExtensionInstalled();
  }
  // The extension should still be disabled.
  EXPECT_FALSE(registrar()->IsExtensionEnabled(extension_id));

  registrar()->EnableExtension(extension_id);
  EXPECT_FALSE(util::AllowFileAccess(extension_id, profile()));
  EXPECT_FALSE(extension->permissions_data()->CanCaptureVisiblePage(
      file_url, tab_id, nullptr, CaptureRequirement::kActiveTabOrAllUrls));
}

TEST_F(ExtensionUtilUnittest, HasIsolatedStorage) {
  // Platform apps should have isolated storage.
  scoped_refptr<const Extension> app =
      ExtensionBuilder("foo_app", ExtensionBuilder::Type::PLATFORM_APP).Build();
  EXPECT_TRUE(app->is_platform_app());
  EXPECT_TRUE(util::HasIsolatedStorage(*app.get(), profile()));

  // Extensions should not have isolated storage.
  scoped_refptr<const Extension> extension =
      ExtensionBuilder("foo_ext").Build();
  EXPECT_FALSE(extension->is_platform_app());
  EXPECT_FALSE(util::HasIsolatedStorage(*extension.get(), profile()));
}

TEST_F(ExtensionUtilUnittest, FixupLongExtensionName) {
  const std::string long_extension_name =
      "A very long extension name etc A very long extension name etc A very "
      "long extension name etc A very long extension name etc";
  std::u16string expected_fixup_extension_name =
      u"A very long extension name etc A very long extension name etc A very "
      u"long\u2026";

  std::u16string fixup_extension_name =
      ui_util::GetFixupExtensionNameForUIDisplay(long_extension_name);
  EXPECT_EQ(fixup_extension_name, expected_fixup_extension_name);
}

using ExtensionUtilDeathTest = testing::Test;

TEST_F(ExtensionUtilDeathTest, GetCWSWritingReviewUrl_InvalidId) {
  const ExtensionId kInvalidId = "invalid_id_format";

  for (const char* invalid_id : {kInvalidId.c_str(), "", "../../etc/passwd"}) {
    EXPECT_CHECK_DEATH(util::GetCWSWritingReviewUrl(
        invalid_id, util::CWSReviewSource::kExtensionsMenu));
  }
}

TEST_F(ExtensionUtilUnittest, GetCWSWritingReviewUrl) {
  const ExtensionId kValidId = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";

  EXPECT_EQ(
      util::GetCWSWritingReviewUrl(
          kValidId, util::CWSReviewSource::kExtensionsMenu)
          .spec(),
      base::StringPrintf("https://chromewebstore.google.com/detail/"
                         "%s/reviews?action=write&source=extensions_menu",
                         kValidId.c_str()));

  EXPECT_EQ(
      util::GetCWSWritingReviewUrl(
          kValidId, util::CWSReviewSource::kExtensionsPage)
          .spec(),
      base::StringPrintf("https://chromewebstore.google.com/detail/"
                         "%s/reviews?action=write&source=extensions_page",
                         kValidId.c_str()));

  EXPECT_EQ(
      util::GetCWSWritingReviewUrl(
          kValidId, util::CWSReviewSource::kContextMenu)
          .spec(),
      base::StringPrintf("https://chromewebstore.google.com/detail/"
                         "%s/reviews?action=write&source=context_menu",
                         kValidId.c_str()));
}
#if BUILDFLAG(IS_CHROMEOS)
class ExtensionUtilWithSigninProfileUnittest : public ExtensionUtilUnittest {
 public:
  void SetUp() override {
    ExtensionUtilUnittest::SetUp();

    auto policy_service = std::make_unique<policy::PolicyServiceImpl>(
        std::vector<
            raw_ptr<policy::ConfigurationPolicyProvider, VectorExperimental>>{
            policy_provider()});
    signin_profile_ = testing_profile_manager()->CreateTestingProfile(
        chrome::kInitialProfile, /*prefs=*/nullptr,
        base::UTF8ToUTF16(
            base::FilePath::StringViewType(chrome::kInitialProfile)),
        0, TestingProfile::TestingFactories(),
        /*is_supervised_profile=*/false, /*is_new_profile=*/std::nullopt,
        std::move(policy_service));
    signin_profile_prefs_ = signin_profile_->GetTestingPrefService();
  }

  void TearDown() override {
    signin_profile_ = nullptr;
    signin_profile_prefs_ = nullptr;
    ExtensionUtilUnittest::TearDown();
  }

  scoped_refptr<const Extension> BuildPolicyInstalledExtension() {
    return ExtensionBuilder("foo_ext")
        .SetLocation(mojom::ManifestLocation::kExternalPolicyDownload)
        .Build();
  }

  void SetupForceList(const ExtensionIdList& extension_ids) {
    base::DictValue dict = base::DictValue();
    for (const auto& extension_id : extension_ids) {
      dict.Set(extension_id,
               base::DictValue().Set(ExternalProviderImpl::kExternalUpdateUrl,
                                     kExtensionUpdateUrl));
    }
    signin_profile_prefs_->SetManagedPref(pref_names::kInstallForceList,
                                          std::move(dict));
  }

 protected:
  raw_ptr<TestingProfile> signin_profile_;

 private:
  raw_ptr<sync_preferences::TestingPrefServiceSyncable> signin_profile_prefs_;
};

// HasIsolatedStorage() will be called when an extension is disabled, more
// precisely when its service worker is unregistered. At that moment the
// extension is already added to the disabled list of the extension registry.
// The method needs to still be able to correctly specify if the extension's
// storage is isolated or not, even if the extension is disabled.
// Regression test for b/279763783.
TEST_F(ExtensionUtilWithSigninProfileUnittest,
       HasIsolatedStorageOnDisabledExtension) {
  scoped_refptr<const Extension> policy_extension =
      BuildPolicyInstalledExtension();
  const std::string& policy_extension_id = policy_extension->id();
  EXPECT_FALSE(policy_extension->is_platform_app());

  // Extension enabled.
  ExtensionRegistry* extension_registry =
      ExtensionRegistry::Get(signin_profile_);
  extension_registry->AddEnabled(policy_extension);
  EXPECT_TRUE(util::HasIsolatedStorage(policy_extension_id, signin_profile_));

  // Extension disabled.
  extension_registry->RemoveEnabled(policy_extension_id);
  extension_registry->AddDisabled(policy_extension);
  EXPECT_TRUE(util::HasIsolatedStorage(policy_extension_id, signin_profile_));

  // Extension neither enabled, nor disabled.
  extension_registry->RemoveDisabled(policy_extension_id);
  EXPECT_FALSE(util::HasIsolatedStorage(policy_extension_id, signin_profile_));
}

TEST_F(ExtensionUtilWithSigninProfileUnittest,
       HasIsolatedStorageOnTerminatedOrBlockedExtension) {
  scoped_refptr<const Extension> policy_extension =
      BuildPolicyInstalledExtension();
  const std::string& policy_extension_id = policy_extension->id();
  EXPECT_FALSE(policy_extension->is_platform_app());

  // Extension enabled.
  ExtensionRegistry* extension_registry =
      ExtensionRegistry::Get(signin_profile_);
  extension_registry->AddEnabled(policy_extension);
  EXPECT_TRUE(util::HasIsolatedStorage(policy_extension_id, signin_profile_));

  // Extension terminated.
  extension_registry->RemoveEnabled(policy_extension_id);
  extension_registry->AddTerminated(policy_extension);
  EXPECT_TRUE(util::HasIsolatedStorage(policy_extension_id, signin_profile_));

  // Extension blocklisted.
  extension_registry->RemoveTerminated(policy_extension_id);
  extension_registry->AddBlocklisted(policy_extension);
  EXPECT_TRUE(util::HasIsolatedStorage(policy_extension_id, signin_profile_));

  // Extension blocked.
  extension_registry->RemoveBlocklisted(policy_extension_id);
  extension_registry->AddBlocked(policy_extension);
  EXPECT_TRUE(util::HasIsolatedStorage(policy_extension_id, signin_profile_));

  // Extension not found.
  extension_registry->RemoveBlocked(policy_extension_id);
  EXPECT_FALSE(util::HasIsolatedStorage(policy_extension_id, signin_profile_));
}

// Verifies that the force-installed extension policy is checked in case it
// was not found in the extension registry. When an extension is unloaded, we
// clean up state from the extension. For service worker-based extensions,
// this includes unregistering the worker, which requires access to the
// storage partition. At this point, since the extension is unloaded, it won't
// be present in the registry, but we still need to determine if the extension
// has isolated storage to pinpoint the correct storage partition.
// Regression test for b/287924795.
TEST_F(ExtensionUtilWithSigninProfileUnittest,
       HasIsolatedStorageForForceInstalledExtensions) {
  scoped_refptr<const Extension> extension1 = BuildPolicyInstalledExtension();
  scoped_refptr<const Extension> extension2 = BuildPolicyInstalledExtension();
  ExtensionRegistry* extension_registry =
      ExtensionRegistry::Get(signin_profile_);
  extension_registry->AddEnabled(extension1);
  extension_registry->AddEnabled(extension2);

  // Extensions are found in the registry, are policy-installed and run on the
  // sign-in screen.
  EXPECT_TRUE(util::HasIsolatedStorage(extension1->id(), signin_profile_));
  EXPECT_TRUE(util::HasIsolatedStorage(extension2->id(), signin_profile_));

  extension_registry->RemoveEnabled(extension1->id());
  extension_registry->RemoveEnabled(extension2->id());

  // Extensions are not found in the registry and are not force-installed.
  EXPECT_FALSE(util::HasIsolatedStorage(extension1->id(), signin_profile_));
  EXPECT_FALSE(util::HasIsolatedStorage(extension2->id(), signin_profile_));

  ExtensionIdList extension_ids;
  extension_ids.push_back(extension1->id());
  extension_ids.push_back(extension2->id());
  SetupForceList(extension_ids);

  // Extensions are not found in the registry, but are force-installed and run
  // on the sign-in screen.
  EXPECT_TRUE(util::HasIsolatedStorage(extension1->id(), signin_profile_));
  EXPECT_TRUE(util::HasIsolatedStorage(extension2->id(), signin_profile_));
}

#endif

}  // namespace extensions
