|  | // Copyright 2014 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/shared_module_service.h" | 
|  |  | 
|  | #include <memory> | 
|  | #include <string> | 
|  | #include <utility> | 
|  |  | 
|  | #include "base/memory/ref_counted.h" | 
|  | #include "base/values.h" | 
|  | #include "chrome/browser/extensions/extension_service.h" | 
|  | #include "chrome/browser/extensions/extension_service_test_base.h" | 
|  | #include "components/crx_file/id_util.h" | 
|  | #include "components/sync/model/string_ordinal.h" | 
|  | #include "components/version_info/version_info.h" | 
|  | #include "extensions/browser/extension_registry.h" | 
|  | #include "extensions/browser/install_flag.h" | 
|  | #include "extensions/browser/pending_extension_manager.h" | 
|  | #include "extensions/browser/uninstall_reason.h" | 
|  | #include "extensions/buildflags/buildflags.h" | 
|  | #include "extensions/common/extension_builder.h" | 
|  | #include "extensions/common/features/feature_channel.h" | 
|  |  | 
|  | static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE)); | 
|  |  | 
|  | namespace extensions { | 
|  |  | 
|  | namespace { | 
|  |  | 
|  | // Return an extension with |id| which imports all the modules that are in the | 
|  | // container |import_ids|. | 
|  | scoped_refptr<const Extension> CreateExtensionImportingModules( | 
|  | const std::vector<std::string>& import_ids, | 
|  | const std::string& id, | 
|  | const std::string& version) { | 
|  | auto builder = base::Value::Dict() | 
|  | .Set("name", "Has Dependent Modules") | 
|  | .Set("version", version) | 
|  | .Set("manifest_version", 2); | 
|  | if (!import_ids.empty()) { | 
|  | base::Value::List import_list; | 
|  | for (const std::string& import_id : import_ids) | 
|  | import_list.Append(base::Value::Dict().Set("id", import_id)); | 
|  | builder.Set("import", std::move(import_list)); | 
|  | } | 
|  | return ExtensionBuilder() | 
|  | .SetManifest(std::move(builder)) | 
|  | .AddFlags(Extension::FROM_WEBSTORE) | 
|  | .SetID(id) | 
|  | .Build(); | 
|  | } | 
|  |  | 
|  | scoped_refptr<const Extension> CreateSharedModule( | 
|  | const std::string& module_id) { | 
|  | base::Value::Dict manifest = | 
|  | base::Value::Dict() | 
|  | .Set("name", "Shared Module") | 
|  | .Set("version", "1.0") | 
|  | .Set("manifest_version", 2) | 
|  | .Set("export", | 
|  | base::Value::Dict().Set("resources", | 
|  | base::Value::List().Append("foo.js"))); | 
|  |  | 
|  | return ExtensionBuilder() | 
|  | .SetManifest(std::move(manifest)) | 
|  | .AddFlags(Extension::FROM_WEBSTORE) | 
|  | .SetID(crx_file::id_util::GenerateId(module_id)) | 
|  | .Build(); | 
|  | } | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  | class SharedModuleServiceUnitTest : public ExtensionServiceTestBase { | 
|  | public: | 
|  | SharedModuleServiceUnitTest() : | 
|  | // The "export" key is open for dev-channel only, but unit tests | 
|  | // run as stable channel on the official Windows build. | 
|  | current_channel_(version_info::Channel::UNKNOWN) {} | 
|  | protected: | 
|  | void SetUp() override; | 
|  |  | 
|  | // Install an extension and notify the ExtensionService. | 
|  | testing::AssertionResult InstallExtension(const Extension* extension, | 
|  | bool is_update); | 
|  | ScopedCurrentChannel current_channel_; | 
|  | }; | 
|  |  | 
|  | void SharedModuleServiceUnitTest::SetUp() { | 
|  | ExtensionServiceTestBase::SetUp(); | 
|  | InitializeGoodInstalledExtensionService(); | 
|  | service()->Init(); | 
|  | } | 
|  |  | 
|  | testing::AssertionResult SharedModuleServiceUnitTest::InstallExtension( | 
|  | const Extension* extension, | 
|  | bool is_update) { | 
|  |  | 
|  | const Extension* old = registry()->GetExtensionById( | 
|  | extension->id(), | 
|  | ExtensionRegistry::ENABLED); | 
|  |  | 
|  | // Verify the extension is not already installed, if it is not update. | 
|  | if (!is_update) { | 
|  | if (old) | 
|  | return testing::AssertionFailure() << "Extension already installed."; | 
|  | } else { | 
|  | if (!old) | 
|  | return testing::AssertionFailure() << "The extension does not exist."; | 
|  | } | 
|  |  | 
|  | // Notify the service that the extension is installed. This adds it to the | 
|  | // registry, notifies interested parties, etc. | 
|  | registrar()->OnExtensionInstalled(extension, syncer::StringOrdinal(), | 
|  | kInstallFlagInstallImmediately); | 
|  |  | 
|  | // Verify that the extension is now installed. | 
|  | if (!registry()->enabled_extensions().Contains(extension->id())) { | 
|  | return testing::AssertionFailure() << "Could not install extension."; | 
|  | } | 
|  |  | 
|  | return testing::AssertionSuccess(); | 
|  | } | 
|  |  | 
|  | TEST_F(SharedModuleServiceUnitTest, AddDependentSharedModules) { | 
|  | // Create an extension that has a dependency. | 
|  | std::string import_id = crx_file::id_util::GenerateId("id"); | 
|  | std::string extension_id = crx_file::id_util::GenerateId("extension_id"); | 
|  | scoped_refptr<const Extension> extension = CreateExtensionImportingModules( | 
|  | std::vector<std::string>(1, import_id), extension_id, "1.0"); | 
|  |  | 
|  | PendingExtensionManager* pending_extension_manager = | 
|  | PendingExtensionManager::Get(profile()); | 
|  |  | 
|  | // Verify that we don't currently want to install the imported module. | 
|  | EXPECT_FALSE(pending_extension_manager->IsIdPending(import_id)); | 
|  |  | 
|  | // Try to satisfy imports for the extension. This should queue the imported | 
|  | // module's installation. | 
|  | SharedModuleService::Get(profile())->SatisfyImports(extension.get()); | 
|  | EXPECT_TRUE(pending_extension_manager->IsIdPending(import_id)); | 
|  | } | 
|  |  | 
|  | TEST_F(SharedModuleServiceUnitTest, PruneSharedModulesOnUninstall) { | 
|  | // Create a module which exports a resource, and install it. | 
|  | scoped_refptr<const Extension> shared_module = | 
|  | CreateSharedModule("shared_module"); | 
|  |  | 
|  | EXPECT_TRUE(InstallExtension(shared_module.get(), false)); | 
|  |  | 
|  | std::string extension_id = crx_file::id_util::GenerateId("extension_id"); | 
|  | // Create and install an extension that imports our new module. | 
|  | scoped_refptr<const Extension> importing_extension = | 
|  | CreateExtensionImportingModules( | 
|  | std::vector<std::string>(1, shared_module->id()), extension_id, | 
|  | "1.0"); | 
|  | EXPECT_TRUE(InstallExtension(importing_extension.get(), false)); | 
|  |  | 
|  | // Uninstall the extension that imports our module. | 
|  | std::u16string error; | 
|  | registrar()->UninstallExtension(importing_extension->id(), | 
|  | UNINSTALL_REASON_FOR_TESTING, &error); | 
|  | EXPECT_TRUE(error.empty()); | 
|  |  | 
|  | // Since the module was only referenced by that single extension, it should | 
|  | // have been uninstalled as a side-effect of uninstalling the extension that | 
|  | // depended upon it. | 
|  | EXPECT_FALSE(registry()->GetExtensionById(shared_module->id(), | 
|  | ExtensionRegistry::EVERYTHING)); | 
|  | } | 
|  |  | 
|  | TEST_F(SharedModuleServiceUnitTest, PruneSharedModulesOnUpdate) { | 
|  | // Create two modules which export a resource, and install them. | 
|  | scoped_refptr<const Extension> shared_module_1 = | 
|  | CreateSharedModule("shared_module_1"); | 
|  | EXPECT_TRUE(InstallExtension(shared_module_1.get(), false)); | 
|  |  | 
|  | base::Value::Dict manifest_2 = | 
|  | base::Value::Dict() | 
|  | .Set("name", "Shared Module 2") | 
|  | .Set("version", "1.0") | 
|  | .Set("manifest_version", 2) | 
|  | .Set("export", | 
|  | base::Value::Dict().Set("resources", | 
|  | base::Value::List().Append("foo.js"))); | 
|  | scoped_refptr<const Extension> shared_module_2 = | 
|  | CreateSharedModule("shared_module_2"); | 
|  | EXPECT_TRUE(InstallExtension(shared_module_2.get(), false)); | 
|  |  | 
|  | std::string extension_id = crx_file::id_util::GenerateId("extension_id"); | 
|  |  | 
|  | // Create and install an extension v1.0 that imports our new module 1. | 
|  | scoped_refptr<const Extension> importing_extension_1 = | 
|  | CreateExtensionImportingModules( | 
|  | std::vector<std::string>(1, shared_module_1->id()), extension_id, | 
|  | "1.0"); | 
|  | EXPECT_TRUE(InstallExtension(importing_extension_1.get(), false)); | 
|  |  | 
|  | // Create and install a new version of the extension that imports our new | 
|  | // module 2. | 
|  | scoped_refptr<const Extension> importing_extension_2 = | 
|  | CreateExtensionImportingModules( | 
|  | std::vector<std::string>(1, shared_module_2->id()), extension_id, | 
|  | "1.1"); | 
|  | EXPECT_TRUE(InstallExtension(importing_extension_2.get(), true)); | 
|  |  | 
|  | // Since the extension v1.1 depends the module 2 instead module 1. | 
|  | // So the module 1 should be uninstalled. | 
|  | EXPECT_FALSE(registry()->GetExtensionById(shared_module_1->id(), | 
|  | ExtensionRegistry::EVERYTHING)); | 
|  | EXPECT_TRUE(registry()->GetExtensionById(shared_module_2->id(), | 
|  | ExtensionRegistry::EVERYTHING)); | 
|  |  | 
|  | // Create and install a new version of the extension that does not import any | 
|  | // module. | 
|  | scoped_refptr<const Extension> importing_extension_3 = | 
|  | CreateExtensionImportingModules(std::vector<std::string>(), extension_id, | 
|  | "1.2"); | 
|  | EXPECT_TRUE(InstallExtension(importing_extension_3.get(), true)); | 
|  |  | 
|  | // Since the extension v1.2 does not depend any module, so the all models | 
|  | // should have been uninstalled. | 
|  | EXPECT_FALSE(registry()->GetExtensionById(shared_module_1->id(), | 
|  | ExtensionRegistry::EVERYTHING)); | 
|  | EXPECT_FALSE(registry()->GetExtensionById(shared_module_2->id(), | 
|  | ExtensionRegistry::EVERYTHING)); | 
|  |  | 
|  | } | 
|  |  | 
|  | TEST_F(SharedModuleServiceUnitTest, AllowlistedImports) { | 
|  | std::string allowlisted_id = crx_file::id_util::GenerateId("allowlisted"); | 
|  | std::string nonallowlisted_id = | 
|  | crx_file::id_util::GenerateId("nonallowlisted"); | 
|  | // Create a module which exports to a restricted allowlist. | 
|  | base::Value::Dict manifest = | 
|  | base::Value::Dict() | 
|  | .Set("name", "Shared Module") | 
|  | .Set("version", "1.0") | 
|  | .Set("manifest_version", 2) | 
|  | .Set("export", | 
|  | base::Value::Dict() | 
|  | .Set("allowlist", base::Value::List().Append(allowlisted_id)) | 
|  | .Set("resources", base::Value::List().Append("*"))); | 
|  | scoped_refptr<const Extension> shared_module = | 
|  | ExtensionBuilder() | 
|  | .SetManifest(std::move(manifest)) | 
|  | .AddFlags(Extension::FROM_WEBSTORE) | 
|  | .SetID(crx_file::id_util::GenerateId("shared_module")) | 
|  | .Build(); | 
|  |  | 
|  | EXPECT_TRUE(InstallExtension(shared_module.get(), false)); | 
|  |  | 
|  | // Create and install an extension with the allowlisted ID. | 
|  | scoped_refptr<const Extension> allowlisted_extension = | 
|  | CreateExtensionImportingModules( | 
|  | std::vector<std::string>(1, shared_module->id()), allowlisted_id, | 
|  | "1.0"); | 
|  | EXPECT_TRUE(InstallExtension(allowlisted_extension.get(), false)); | 
|  |  | 
|  | // Try to install an extension with an ID that is not allowlisted. | 
|  | scoped_refptr<const Extension> nonallowlisted_extension = | 
|  | CreateExtensionImportingModules( | 
|  | std::vector<std::string>(1, shared_module->id()), nonallowlisted_id, | 
|  | "1.0"); | 
|  | // This should succeed because only CRX installer (and by extension the | 
|  | // WebStore Installer) checks the shared module allowlist.  InstallExtension | 
|  | // bypasses the allowlist check because the SharedModuleService does not | 
|  | // care about allowlists. | 
|  | EXPECT_TRUE(InstallExtension(nonallowlisted_extension.get(), false)); | 
|  | } | 
|  |  | 
|  | TEST_F(SharedModuleServiceUnitTest, PruneMultipleSharedModules) { | 
|  | // Create two modules which export a resource each, and install it. | 
|  | scoped_refptr<const Extension> shared_module_one = | 
|  | CreateSharedModule("shared_module_one"); | 
|  | EXPECT_TRUE(InstallExtension(shared_module_one.get(), false)); | 
|  | scoped_refptr<const Extension> shared_module_two = | 
|  | CreateSharedModule("shared_module_two"); | 
|  | EXPECT_TRUE(InstallExtension(shared_module_two.get(), false)); | 
|  |  | 
|  | std::string extension_id = crx_file::id_util::GenerateId("extension_id"); | 
|  | std::vector<std::string> module_ids; | 
|  | module_ids.push_back(shared_module_one->id()); | 
|  | module_ids.push_back(shared_module_two->id()); | 
|  | // Create and install an extension that imports both the modules. | 
|  | scoped_refptr<const Extension> importing_extension = | 
|  | CreateExtensionImportingModules(module_ids, extension_id, "1.0"); | 
|  | EXPECT_TRUE(InstallExtension(importing_extension.get(), false)); | 
|  |  | 
|  | // Uninstall the extension that imports our modules. | 
|  | std::u16string error; | 
|  | registrar()->UninstallExtension(importing_extension->id(), | 
|  | UNINSTALL_REASON_FOR_TESTING, &error); | 
|  | EXPECT_TRUE(error.empty()); | 
|  |  | 
|  | // Since the modules were only referenced by that single extension, they | 
|  | // should have been uninstalled as a side-effect of uninstalling the extension | 
|  | // that depended upon it. | 
|  | EXPECT_FALSE(registry()->GetExtensionById(shared_module_one->id(), | 
|  | ExtensionRegistry::EVERYTHING)); | 
|  | EXPECT_FALSE(registry()->GetExtensionById(shared_module_two->id(), | 
|  | ExtensionRegistry::EVERYTHING)); | 
|  | } | 
|  |  | 
|  | }  // namespace extensions |