blob: 60e4a72cc5a9895612f250bae6814607a0882ba6 [file] [log] [blame]
// 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 "chrome/browser/extensions/component_loader.h"
#include <stddef.h>
#include <memory>
#include <optional>
#include <string>
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/memory/raw_ptr.h"
#include "base/one_shot_event.h"
#include "base/path_service.h"
#include "base/scoped_observation.h"
#include "base/strings/stringprintf.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/extensions/chrome_extension_registrar_delegate.h"
#include "chrome/browser/extensions/extension_service_user_test_base.h"
#include "chrome/browser/extensions/test_extension_system.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile.h"
#include "chrome/test/base/testing_profile_manager.h"
#include "components/sync_preferences/testing_pref_service_syncable.h"
#include "content/public/test/browser_task_environment.h"
#include "extensions/browser/extension_registrar.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_registry_observer.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/unloaded_extension_reason.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/constants.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_resource.h"
#include "extensions/common/extension_set.h"
#include "extensions/common/manifest_handlers/background_info.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/display/test/test_screen.h"
static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));
namespace extensions {
class ExtensionUnloadedObserver : public ExtensionRegistryObserver {
public:
explicit ExtensionUnloadedObserver(ExtensionRegistry* registry) {
observation_.Observe(registry);
}
ExtensionUnloadedObserver(const ExtensionUnloadedObserver&) = delete;
ExtensionUnloadedObserver& operator=(const ExtensionUnloadedObserver&) =
delete;
size_t unloaded_count() const { return unloaded_count_; }
protected:
void OnExtensionUnloaded(content::BrowserContext* browser_context,
const Extension* extension,
UnloadedExtensionReason reason) override {
ASSERT_TRUE(Manifest::IsComponentLocation(extension->location()));
++unloaded_count_;
}
private:
size_t unloaded_count_ = 0;
base::ScopedObservation<ExtensionRegistry, ExtensionRegistryObserver>
observation_{this};
};
// TODO(crbug.com/408458901): Use an extensions test base class once we have
// one that works on desktop Android.
class ComponentLoaderTest : public testing::Test {
public:
ComponentLoaderTest() = default;
void SetUp() override {
profile_manager_ = std::make_unique<TestingProfileManager>(
TestingBrowserProcess::GetGlobal());
ASSERT_TRUE(profile_manager_->SetUp());
profile_ = profile_manager_->CreateTestingProfile(
TestingProfile::kDefaultProfileUserName, /*prefs=*/nullptr,
/*user_name=*/std::u16string(),
/*avatar_id=*/0, /*testing_factories=*/{});
extension_system_ =
static_cast<TestExtensionSystem*>(ExtensionSystem::Get(profile()));
extension_path_ =
GetBasePath().AppendASCII("good")
.AppendASCII("Extensions")
.AppendASCII("behllobkkfkfnphdnhnkndlbkcpglgmj")
.AppendASCII("1.0.0.0");
// Read in the extension manifest.
ASSERT_TRUE(base::ReadFileToString(
extension_path_.Append(kManifestFilename),
&manifest_contents_));
component_loader_ = ComponentLoader::Get(profile());
component_loader_->set_ignore_allowlist_for_testing(true);
// Set up ExtensionRegistrar with a delegate.
extension_registrar_delegate_ =
std::make_unique<ChromeExtensionRegistrarDelegate>(profile_.get());
extension_registrar_ = ExtensionRegistrar::Get(profile_.get());
extension_registrar_->Init(extension_registrar_delegate_.get(), true,
base::CommandLine::ForCurrentProcess(),
base::FilePath(), base::FilePath());
extension_registrar_delegate_->Init(extension_registrar_);
}
void TearDown() override {
extension_registrar_delegate_->Shutdown();
extension_registrar_ = nullptr;
component_loader_ = nullptr;
extension_system_ = nullptr;
profile_ = nullptr;
profile_manager_.reset();
}
base::FilePath GetBasePath() {
base::FilePath test_data_dir;
base::PathService::Get(chrome::DIR_TEST_DATA, &test_data_dir);
return test_data_dir.AppendASCII("extensions");
}
void UninstallAllExtensions() {
auto* registry = ExtensionRegistry::Get(profile());
ExtensionSet extensions = registry->GenerateInstalledExtensionsSet();
for (const auto& extension : extensions) {
extension_registrar_->RemoveExtension(extension->id(),
UnloadedExtensionReason::UNINSTALL);
}
}
TestingProfile* profile() { return profile_; }
protected:
content::BrowserTaskEnvironment task_environment_{
base::test::TaskEnvironment::TimeSource::MOCK_TIME};
std::unique_ptr<TestingProfileManager> profile_manager_;
raw_ptr<TestingProfile> profile_ = nullptr;
std::unique_ptr<ChromeExtensionRegistrarDelegate>
extension_registrar_delegate_;
raw_ptr<ExtensionRegistrar> extension_registrar_ = nullptr;
raw_ptr<TestExtensionSystem> extension_system_ = nullptr;
raw_ptr<ComponentLoader> component_loader_ = nullptr;
// The root directory of the text extension.
base::FilePath extension_path_;
// The contents of the text extension's manifest file.
std::string manifest_contents_;
#if BUILDFLAG(IS_ANDROID)
// WebContentImpl requires a Screen instance on Android.
display::test::TestScreen screen_{/*create_display=*/true,
/*register_screen=*/true};
#endif
};
TEST_F(ComponentLoaderTest, ParseManifest) {
std::optional<base::Value::Dict> manifest;
// Test invalid JSON.
manifest = component_loader_->ParseManifest("{ 'test': 3 } invalid");
EXPECT_FALSE(manifest);
// Test manifests that are valid JSON, but don't have an object literal
// at the root. ParseManifest() should always return NULL.
manifest = component_loader_->ParseManifest(std::string());
EXPECT_FALSE(manifest);
manifest = component_loader_->ParseManifest("[{ \"foo\": 3 }]");
EXPECT_FALSE(manifest);
manifest = component_loader_->ParseManifest("\"Test\"");
EXPECT_FALSE(manifest);
manifest = component_loader_->ParseManifest("42");
EXPECT_FALSE(manifest);
manifest = component_loader_->ParseManifest("true");
EXPECT_FALSE(manifest);
manifest = component_loader_->ParseManifest("false");
EXPECT_FALSE(manifest);
manifest = component_loader_->ParseManifest("null");
EXPECT_FALSE(manifest);
// Test parsing valid JSON.
manifest = component_loader_->ParseManifest(
"{ \"test\": { \"one\": 1 }, \"two\": 2 }");
ASSERT_TRUE(manifest);
EXPECT_EQ(1, manifest->FindIntByDottedPath("test.one"));
EXPECT_EQ(2, manifest->FindInt("two"));
manifest = component_loader_->ParseManifest(manifest_contents_);
const std::string* string_value =
manifest->FindStringByDottedPath("background.page");
ASSERT_TRUE(string_value);
EXPECT_EQ("backgroundpage.html", *string_value);
}
// Test that the extension isn't loaded if the extension service isn't ready.
TEST_F(ComponentLoaderTest, AddWhenNotReady) {
std::string extension_id =
component_loader_->Add(manifest_contents_, extension_path_);
EXPECT_NE("", extension_id);
ExtensionRegistry* registry = ExtensionRegistry::Get(profile());
EXPECT_EQ(0u, registry->enabled_extensions().size());
}
// Test that it *is* loaded when the extension service *is* ready.
TEST_F(ComponentLoaderTest, AddWhenReady) {
extension_system_->SetReady();
std::string extension_id =
component_loader_->Add(manifest_contents_, extension_path_);
EXPECT_NE("", extension_id);
ExtensionRegistry* registry = ExtensionRegistry::Get(profile());
EXPECT_EQ(1u, registry->enabled_extensions().size());
EXPECT_TRUE(registry->enabled_extensions().GetByID(extension_id));
}
TEST_F(ComponentLoaderTest, Remove) {
ExtensionRegistry* registry = ExtensionRegistry::Get(profile());
// Removing an extension that was never added should be ok.
component_loader_->Remove(extension_path_);
EXPECT_EQ(0u, registry->enabled_extensions().size());
// Try adding and removing before LoadAll() is called.
component_loader_->Add(manifest_contents_, extension_path_);
component_loader_->Remove(extension_path_);
component_loader_->LoadAll();
EXPECT_EQ(0u, registry->enabled_extensions().size());
// Load an extension, and check that it's unloaded when Remove() is called.
extension_system_->SetReady();
std::string extension_id =
component_loader_->Add(manifest_contents_, extension_path_);
EXPECT_EQ(1u, registry->enabled_extensions().size());
component_loader_->Remove(extension_path_);
EXPECT_EQ(0u, registry->enabled_extensions().size());
// And after calling LoadAll(), it shouldn't get loaded.
component_loader_->LoadAll();
EXPECT_EQ(0u, registry->enabled_extensions().size());
}
TEST_F(ComponentLoaderTest, LoadAll) {
ExtensionRegistry* registry = ExtensionRegistry::Get(profile());
// No extensions should be loaded if none were added.
component_loader_->LoadAll();
EXPECT_EQ(0u, registry->enabled_extensions().size());
// Use LoadAll() to load the default extensions.
component_loader_->AddDefaultComponentExtensions(false);
component_loader_->LoadAll();
unsigned int default_count = registry->enabled_extensions().size();
// Clear the list of loaded extensions, and reload with one more.
UninstallAllExtensions();
component_loader_->Add(manifest_contents_, extension_path_);
component_loader_->LoadAll();
EXPECT_EQ(default_count + 1, registry->enabled_extensions().size());
}
class ComponentLoaderExtensionServiceTest
: public ExtensionServiceUserTestBase {
public:
// testing::Test:
void SetUp() override {
ExtensionServiceUserTestBase::InitializeEmptyExtensionService();
ExtensionServiceUserTestBase::SetUp();
}
// Test that certain histograms are emitted for user and non-user profiles
// (users for ChromeOS Ash).
void RunEmitUserHistogramsTest(int nonuser_expected_total_count,
int user_expected_total_count) {
auto* component_loader = ComponentLoader::Get(profile());
component_loader->set_profile_for_testing(profile());
base::HistogramTester histograms;
component_loader->LoadAll();
histograms.ExpectTotalCount("Extensions.LoadAllComponentTime", 1);
histograms.ExpectTotalCount("Extensions.LoadAllComponentTime.NonUser",
nonuser_expected_total_count);
histograms.ExpectTotalCount("Extensions.LoadAllComponentTime.User",
user_expected_total_count);
}
};
TEST_F(ComponentLoaderExtensionServiceTest, LoadAll_EmitUserHistograms) {
ASSERT_NO_FATAL_FAILURE(MaybeSetUpTestUser(/*is_guest=*/false));
RunEmitUserHistogramsTest(/*nonuser_expected_total_count=*/0,
/*user_expected_total_count=*/1);
}
TEST_F(ComponentLoaderExtensionServiceTest, LoadAll_NonUserEmitHistograms) {
ASSERT_NO_FATAL_FAILURE(MaybeSetUpTestUser(/*is_guest=*/true));
RunEmitUserHistogramsTest(/*nonuser_expected_total_count=*/1,
/*user_expected_total_count=*/0);
}
// Test is flaky. https://crbug.com/1306983
TEST_F(ComponentLoaderTest, DISABLED_AddOrReplace) {
ExtensionRegistry* registry = ExtensionRegistry::Get(profile());
ExtensionUnloadedObserver unload_observer(registry);
EXPECT_EQ(0u, component_loader_->registered_extensions_count());
// Allow the Feedback extension, which has a background page, to be loaded.
component_loader_->EnableBackgroundExtensionsForTesting();
component_loader_->AddDefaultComponentExtensions(false);
size_t const default_count = component_loader_->registered_extensions_count();
base::FilePath known_extension = GetBasePath()
.AppendASCII("override_component_extension");
base::FilePath unknown_extension = extension_path_;
base::FilePath invalid_extension = GetBasePath()
.AppendASCII("this_path_does_not_exist");
// Replace a default component extension.
component_loader_->AddOrReplace(known_extension);
EXPECT_EQ(default_count, component_loader_->registered_extensions_count());
// Add a new component extension.
component_loader_->AddOrReplace(unknown_extension);
EXPECT_EQ(default_count + 1,
component_loader_->registered_extensions_count());
extension_system_->SetReady();
component_loader_->LoadAll();
EXPECT_EQ(default_count + 1, registry->enabled_extensions().size());
EXPECT_EQ(0u, unload_observer.unloaded_count());
// replace loaded component extension.
component_loader_->AddOrReplace(known_extension);
EXPECT_EQ(default_count + 1, registry->enabled_extensions().size());
EXPECT_EQ(1u, unload_observer.unloaded_count());
// Add an invalid component extension.
std::string extension_id = component_loader_->AddOrReplace(invalid_extension);
EXPECT_TRUE(extension_id.empty());
}
#if BUILDFLAG(IS_CHROMEOS)
// Tests that component extensions follow symbolic links for its resources.
TEST_F(ComponentLoaderTest, FollowSymlinks) {
base::ScopedTempDir temp_dir;
ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
const base::FilePath extension_dir =
temp_dir.GetPath().AppendASCII("extension");
ASSERT_TRUE(base::CreateDirectory(extension_dir));
constexpr char kKey[] =
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0wsqv0aItfmI5B5"
"fTOChbevC1X5Xm/2et2H1TpocRQGSz5oGhVMUPjGgBeYvHemCkBGiuFYQgT"
"62aPZLNqQG96dFEG4GGuY/FX7Kq1G9NF1ovZ9H5JMniEj/zfj1W64mCwgFI"
"uVN8b3m+vs3ZoCX9OD4s9mEEEiygqltfsxhGBAmPLdDcY6Ze9/FHNJh6Q6s"
"Aa6hIhfmmbLSyvX/kL3359J1yVTFuW1SK2/hfDINl+MWkHgd2xV0tkl/bVy"
"NlH4XtoK3ZedHe7mrkiEoO65U/PpmN/coLoE07C/xkQwYVqbQWllq2Ij8j2"
"XkJdwv3qrcDsOvvB1W498CyMkuGL2UswIDAQAB";
constexpr char kManifestTemplate[] =
R"({
"key": "%s",
"version": "1.0",
"manifest_version": 3,
"name": "Test extension"
})";
ASSERT_TRUE(base::WriteFile(extension_dir.Append(kManifestFilename),
base::StringPrintf(kManifestTemplate, kKey)));
const base::FilePath shared_data = temp_dir.GetPath().AppendASCII("data");
ASSERT_TRUE(base::WriteFile(shared_data, "shared tests file"));
const base::FilePath rel_path_in_extension_root(FILE_PATH_LITERAL("link"));
base::FilePath link_in_extension_root =
extension_dir.Append(rel_path_in_extension_root);
ASSERT_TRUE(base::CreateSymbolicLink(shared_data, link_in_extension_root));
extension_system_->SetReady();
const ExtensionId extension_id =
component_loader_->AddOrReplace(extension_dir);
ASSERT_NE("", extension_id);
ExtensionRegistry* registry = ExtensionRegistry::Get(profile());
const Extension* extension =
registry->enabled_extensions().GetByID(extension_id);
ASSERT_TRUE(extension);
const base::FilePath resource_path =
extension->GetResource(rel_path_in_extension_root).GetFilePath();
EXPECT_EQ(base::MakeAbsoluteFilePath(resource_path),
base::MakeAbsoluteFilePath(shared_data));
}
#endif // BUILDFLAG(IS_CHROMEOS)
} // namespace extensions