// 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 <optional>
#include <string>
#include <string_view>

#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/json/json_reader.h"
#include "base/logging.h"
#include "base/metrics/histogram_macros.h"
#include "base/path_service.h"
#include "base/time/time.h"
#include "base/trace_event/trace_event.h"
#include "base/values.h"
#include "build/branding_buildflags.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/extensions/component_extensions_allowlist/allowlist.h"
#include "chrome/browser/extensions/component_loader_factory.h"
#include "chrome/browser/extensions/data_deleter.h"
#include "chrome/browser/extensions/glic_util.h"
#include "chrome/browser/extensions/profile_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/channel_info.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/extensions/extension_constants.h"
#include "chrome/grit/aim_eligibility_extension_resources.h"
#include "chrome/grit/branded_strings.h"
#include "chrome/grit/browser_resources.h"
#include "chrome/grit/component_extension_resources.h"
#include "chrome/grit/contextual_tasks_extension_resources.h"
#include "chrome/grit/generated_resources.h"
#include "components/crx_file/id_util.h"
#include "components/omnibox/common/omnibox_features.h"
#include "components/version_info/version_info.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/common/content_switches.h"
#include "extensions/browser/extension_file_task_runner.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_registrar.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/pref_names.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/constants.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_features.h"
#include "extensions/common/extension_id.h"
#include "extensions/common/extension_l10n_util.h"
#include "extensions/common/file_util.h"
#include "extensions/common/manifest_constants.h"
#include "pdf/buildflags.h"
#include "printing/buildflags/buildflags.h"
#include "ui/accessibility/accessibility_features.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"

#if BUILDFLAG(IS_CHROMEOS)
#include "ash/constants/ash_extension_constants.h"
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/constants/ash_switches.h"
#include "ash/keyboard/ui/grit/keyboard_resources.h"
#include "base/system/sys_info.h"
#include "chromeos/constants/chromeos_features.h"
#include "components/user_manager/user_manager.h"
#include "content/public/browser/site_instance.h"
#include "content/public/browser/storage_partition.h"
#include "extensions/browser/extensions_browser_client.h"
#include "extensions/common/switches.h"
#include "storage/browser/file_system/file_system_context.h"
#include "ui/file_manager/grit/file_manager_resources.h"
#endif

#if BUILDFLAG(ENABLE_PDF)
#include "chrome/browser/pdf/pdf_extension_util.h"
#endif

#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
#include "chrome/browser/defaults.h"
#endif

static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));

using content::BrowserThread;

namespace extensions {

namespace {

#if BUILDFLAG(ENABLE_HANGOUT_SERVICES_EXTENSION)
BASE_FEATURE(kHangoutsExtensionV3, base::FEATURE_DISABLED_BY_DEFAULT);
#endif  // BUILDFLAG(ENABLE_HANGOUT_SERVICES_EXTENSION)

bool g_enable_background_extensions_during_testing = false;

#if BUILDFLAG(IS_CHROMEOS) && BUILDFLAG(GOOGLE_CHROME_BRANDING)
// Whether HelpApp is enabled.
bool g_enable_help_app = true;
#endif

ExtensionId GenerateId(const base::DictValue& manifest,
                       const base::FilePath& path) {
  std::string id_input;
  const std::string* raw_key = manifest.FindString(manifest_keys::kPublicKey);
  CHECK(raw_key != nullptr);
  CHECK(Extension::ParsePEMKeyBytes(*raw_key, &id_input));
  ExtensionId id = crx_file::id_util::GenerateId(id_input);
  return id;
}

#if BUILDFLAG(IS_CHROMEOS)
std::optional<base::DictValue> LoadManifestOnFileThread(
    const base::FilePath& root_directory,
    const base::FilePath::CharType* manifest_filename,
    bool localize_manifest) {
  DCHECK(GetExtensionFileTaskRunner()->RunsTasksInCurrentSequence());
  std::string error;
  std::optional<base::DictValue> manifest(
      file_util::LoadManifest(root_directory, manifest_filename, &error));
  if (!manifest) {
    LOG(ERROR) << "Can't load "
               << root_directory.Append(manifest_filename).AsUTF8Unsafe()
               << ": " << error;
    return std::nullopt;
  }

  if (localize_manifest) {
    // This is only called for Chrome OS component extensions which are loaded
    // from a read-only rootfs partition, so it is safe to set
    // |gzip_permission| to kAllowForTrustedSource.
    bool localized = extension_l10n_util::LocalizeExtension(
        root_directory, &manifest.value(),
        extension_l10n_util::GzippedMessagesPermission::kAllowForTrustedSource,
        &error);
    CHECK(localized) << error;
  }

  return manifest;
}
#endif  // BUILDFLAG(IS_CHROMEOS)

#if BUILDFLAG(IS_CHROMEOS)
bool IsNormalSession() {
  return !base::CommandLine::ForCurrentProcess()->HasSwitch(
             ash::switches::kGuestSession) &&
         user_manager::UserManager::IsInitialized() &&
         user_manager::UserManager::Get()->IsUserLoggedIn();
}
#endif  // BUILDFLAG(IS_CHROMEOS)

}  // namespace

ComponentLoader::ComponentExtensionInfo::ComponentExtensionInfo(
    base::DictValue manifest_param,
    const base::FilePath& directory)
    : manifest(std::move(manifest_param)), root_directory(directory) {
  if (!root_directory.IsAbsolute()) {
    CHECK(base::PathService::Get(chrome::DIR_RESOURCES, &root_directory));
    root_directory = root_directory.Append(directory);
  }
  extension_id = GenerateId(manifest, root_directory);
}

ComponentLoader::ComponentExtensionInfo::ComponentExtensionInfo(
    ComponentExtensionInfo&& other)
    : manifest(std::move(other.manifest)),
      root_directory(std::move(other.root_directory)),
      extension_id(std::move(other.extension_id)) {}

ComponentLoader::ComponentExtensionInfo&
ComponentLoader::ComponentExtensionInfo::operator=(
    ComponentExtensionInfo&& other) {
  manifest = std::move(other.manifest);
  root_directory = std::move(other.root_directory);
  extension_id = std::move(other.extension_id);
  return *this;
}

ComponentLoader::ComponentExtensionInfo::~ComponentExtensionInfo() = default;

// static
ComponentLoader* ComponentLoader::Get(content::BrowserContext* context) {
  return ComponentLoaderFactory::GetForBrowserContext(context);
}

ComponentLoader::ComponentLoader(Profile* profile)
    : profile_(profile),
      extension_system_(ExtensionSystem::Get(profile_)),
      ignore_allowlist_for_testing_(false) {}

ComponentLoader::~ComponentLoader() = default;

void ComponentLoader::Shutdown() {
  profile_ = nullptr;
  extension_system_ = nullptr;
}

void ComponentLoader::LoadAll() {
  TRACE_EVENT0("browser,startup", "ComponentLoader::LoadAll");
  bool is_user_profile =
      profile_util::ProfileCanUseNonComponentExtensions(profile_);
  const base::TimeTicks load_start_time = base::TimeTicks::Now();

  for (const auto& component_extension : component_extensions_) {
    Load(component_extension);
  }

  const base::TimeDelta load_all_component_time =
      base::TimeTicks::Now() - load_start_time;
  UMA_HISTOGRAM_TIMES("Extensions.LoadAllComponentTime",
                      load_all_component_time);
  if (is_user_profile) {
    UMA_HISTOGRAM_TIMES("Extensions.LoadAllComponentTime.User",
                        load_all_component_time);
  } else {
    UMA_HISTOGRAM_TIMES("Extensions.LoadAllComponentTime.NonUser",
                        load_all_component_time);
  }
}

std::optional<base::DictValue> ComponentLoader::ParseManifest(
    std::string_view manifest_contents) const {
  std::optional<base::DictValue> manifest = base::JSONReader::ReadDict(
      manifest_contents, base::JSON_PARSE_CHROMIUM_EXTENSIONS);
  if (!manifest) {
    LOG(ERROR) << "Failed to parse extension manifest.";
    return std::nullopt;
  }
  return manifest;
}

ExtensionId ComponentLoader::Add(int manifest_resource_id,
                                 const base::FilePath& root_directory) {
  if (!ignore_allowlist_for_testing_ &&
      !IsComponentExtensionAllowlisted(manifest_resource_id)) {
    return std::string();
  }

  std::string manifest_contents =
      ui::ResourceBundle::GetSharedInstance().LoadDataResourceString(
          manifest_resource_id);
  return Add(manifest_contents, root_directory, true);
}

ExtensionId ComponentLoader::Add(base::DictValue manifest,
                                 const base::FilePath& root_directory) {
  return Add(std::move(manifest), root_directory, false);
}

ExtensionId ComponentLoader::Add(std::string_view manifest_contents,
                                 const base::FilePath& root_directory) {
  return Add(manifest_contents, root_directory, false);
}

ExtensionId ComponentLoader::Add(std::string_view manifest_contents,
                                 const base::FilePath& root_directory,
                                 bool skip_allowlist) {
  // The Value is kept for the lifetime of the ComponentLoader. This is
  // required in case LoadAll() is called again.
  std::optional<base::DictValue> manifest = ParseManifest(manifest_contents);
  if (manifest) {
    return Add(std::move(*manifest), root_directory, skip_allowlist);
  }
  return std::string();
}

ExtensionId ComponentLoader::Add(base::DictValue parsed_manifest,
                                 const base::FilePath& root_directory,
                                 bool skip_allowlist) {
  ComponentExtensionInfo info(std::move(parsed_manifest), root_directory);
  if (!ignore_allowlist_for_testing_ && !skip_allowlist &&
      !IsComponentExtensionAllowlisted(info.extension_id)) {
    return std::string();
  }

  component_extensions_.push_back(std::move(info));
  ComponentExtensionInfo& added_info = component_extensions_.back();
  if (extension_system_->is_ready()) {
    Load(added_info);
  }
  return added_info.extension_id;
}

ExtensionId ComponentLoader::AddOrReplace(const base::FilePath& path) {
  base::FilePath absolute_path = base::MakeAbsoluteFilePath(path);
  std::string error;
  std::optional<base::DictValue> manifest(
      file_util::LoadManifest(absolute_path, &error));
  if (!manifest) {
    LOG(ERROR) << "Could not load extension from '" << absolute_path.value()
               << "'. " << error;
    return std::string();
  }
  Remove(GenerateId(*manifest, absolute_path));

  // We don't check component extensions loaded by path because this is only
  // used by developers for testing.
  return Add(std::move(*manifest), absolute_path, true);
}

void ComponentLoader::Reload(const ExtensionId& extension_id) {
  for (const auto& component_extension : component_extensions_) {
    if (component_extension.extension_id == extension_id) {
      Load(component_extension);
      break;
    }
  }
}

void ComponentLoader::Load(const ComponentExtensionInfo& info) {
  std::u16string error;
  scoped_refptr<const Extension> extension(CreateExtension(info, &error));
  if (!extension.get()) {
    LOG(ERROR) << error;
    return;
  }

  CHECK_EQ(info.extension_id, extension->id()) << extension->name();
  auto* registrar = ExtensionRegistrar::Get(profile_);
  registrar->AddComponentExtension(extension.get());
}

void ComponentLoader::Remove(const base::FilePath& root_directory) {
  // Find the ComponentExtensionInfo for the extension.
  for (const auto& component_extension : component_extensions_) {
    if (component_extension.root_directory == root_directory) {
      Remove(GenerateId(component_extension.manifest, root_directory));
      break;
    }
  }
}

void ComponentLoader::Remove(const ExtensionId& id) {
  for (auto it = component_extensions_.begin();
       it != component_extensions_.end(); ++it) {
    if (it->extension_id == id) {
      UnloadComponent(&(*it));
      component_extensions_.erase(it);
      break;
    }
  }

#if BUILDFLAG(IS_CHROMEOS)
  if (IsPendingAdd(id)) {
    pending_extension_ids_.erase(id);
  }
#endif  // BUILDFLAG(IS_CHROMEOS)
}

bool ComponentLoader::Exists(const ExtensionId& id) const {
  for (const auto& component_extension : component_extensions_) {
    if (component_extension.extension_id == id) {
      return true;
    }
  }
  return false;
}

std::vector<ExtensionId> ComponentLoader::GetRegisteredComponentExtensionsIds()
    const {
  std::vector<ExtensionId> result;
  for (const auto& el : component_extensions_) {
    result.push_back(el.extension_id);
  }
  return result;
}

#if BUILDFLAG(ENABLE_HANGOUT_SERVICES_EXTENSION)
void ComponentLoader::AddHangoutServicesExtension() {
  // Finch controlled migration to a v3 Manifest - see crbug.com/326877912.
  if (base::FeatureList::IsEnabled(kHangoutsExtensionV3)) {
    Add(IDR_HANGOUT_SERVICES_MANIFEST_V3,
        base::FilePath(FILE_PATH_LITERAL("hangout_services")));
  } else {
    Add(IDR_HANGOUT_SERVICES_MANIFEST_V2,
        base::FilePath(FILE_PATH_LITERAL("hangout_services")));
  }
}
#endif  // BUILDFLAG(ENABLE_HANGOUT_SERVICES_EXTENSION)

void ComponentLoader::AddNetworkSpeechSynthesisExtension() {
  if (::features::IsExtensionManifestV3NetworkSpeechSynthesisEnabled()) {
    Add(IDR_NETWORK_SPEECH_SYNTHESIS_MANIFEST_MV3,
        base::FilePath(FILE_PATH_LITERAL("network_speech_synthesis/mv3")));
  } else {
    Add(IDR_NETWORK_SPEECH_SYNTHESIS_MANIFEST,
        base::FilePath(FILE_PATH_LITERAL("network_speech_synthesis")));
  }
}

void ComponentLoader::AddAimEligibilityExtension() {
  if (base::FeatureList::IsEnabled(
          omnibox::kAimEligibilityComponentExtension)) {
    Add(IDR_AIM_ELIGIBILITY_EXTENSION_MANIFEST_JSON,
        base::FilePath(FILE_PATH_LITERAL("aim_eligibility_extension")));
  }
}

void ComponentLoader::AddGlicExtension() {
  if (IsApiGlicPrivateEnabled()) {
    Add(IDR_GLIC_EXTENSION_MANIFEST,
        base::FilePath(FILE_PATH_LITERAL("glic_extension")));
  }
}

void ComponentLoader::AddContextualTasksExtension() {
  if (base::FeatureList::IsEnabled(
          extensions_features::kApiContextualTasksPrivate)) {
    Add(IDR_CONTEXTUAL_TASKS_EXTENSION_MANIFEST_JSON,
        base::FilePath(FILE_PATH_LITERAL("contextual_tasks_extension")));
  }
}

void ComponentLoader::AddWithNameAndDescription(
    int manifest_resource_id,
    const base::FilePath& root_directory,
    const std::string& name_string,
    const std::string& description_string) {
  if (!ignore_allowlist_for_testing_ &&
      !IsComponentExtensionAllowlisted(manifest_resource_id)) {
    return;
  }

  std::string manifest_contents =
      ui::ResourceBundle::GetSharedInstance().LoadDataResourceString(
          manifest_resource_id);

  // The Value is kept for the lifetime of the ComponentLoader. This is
  // required in case LoadAll() is called again.
  std::optional<base::DictValue> manifest = ParseManifest(manifest_contents);

  if (manifest) {
    manifest->Set(manifest_keys::kName, name_string);
    manifest->Set(manifest_keys::kDescription, description_string);
    Add(std::move(*manifest), root_directory, true);
  }
}

void ComponentLoader::AddWebStoreApp() {
#if BUILDFLAG(IS_CHROMEOS)
  if (!IsNormalSession()) {
    return;
  }
#endif

  if (base::FeatureList::IsEnabled(extensions_features::kWebstoreHostedApp)) {
    AddWithNameAndDescription(
        IDR_WEBSTORE_MANIFEST, base::FilePath(FILE_PATH_LITERAL("web_store")),
        l10n_util::GetStringUTF8(IDS_WEBSTORE_NAME_STORE),
        l10n_util::GetStringUTF8(IDS_WEBSTORE_APP_DESCRIPTION));
  }
}

#if BUILDFLAG(IS_CHROMEOS)
void ComponentLoader::AddChromeApp() {
  AddWithNameAndDescription(
      IDR_CHROME_APP_MANIFEST, base::FilePath(FILE_PATH_LITERAL("chrome_app")),
      l10n_util::GetStringUTF8(IDS_SHORT_PRODUCT_NAME),
      l10n_util::GetStringUTF8(IDS_CHROME_SHORTCUT_DESCRIPTION));
}

void ComponentLoader::AddImageLoaderExtension() {
  Add(IDR_IMAGE_LOADER_MANIFEST,
      base::FilePath(FILE_PATH_LITERAL("image_loader")));
}

void ComponentLoader::AddGuestModeTestExtension(const base::FilePath& path) {
  base::SysInfo::CrashIfChromeOSNonTestImage();
  AddComponentFromDirWithManifestFilename(
      path, extension_misc::kGuestModeTestExtensionId,
      extensions::kManifestFilename, extensions::kManifestFilename,
      /*done_cb=*/{}, /*error_cb=*/{});
}

void ComponentLoader::AddKeyboardApp() {
  Add(IDR_KEYBOARD_MANIFEST, base::FilePath(FILE_PATH_LITERAL("keyboard")));
}

#endif  // BUILDFLAG(IS_CHROMEOS)

scoped_refptr<const Extension> ComponentLoader::CreateExtension(
    const ComponentExtensionInfo& info,
    std::u16string* error) {
  // TODO(abarth): We should REQUIRE_MODERN_MANIFEST_VERSION once we've updated
  //               our component extensions to the new manifest version.
  int flags = Extension::REQUIRE_KEY;

#if BUILDFLAG(IS_CHROMEOS)
  // ChromeOS component extension (GoogleTTS) needs to use symlinks to share
  // data during MV2 to MV3 migration.
  flags |= Extension::FOLLOW_SYMLINKS_ANYWHERE;
#endif

  return Extension::Create(info.root_directory,
                           mojom::ManifestLocation::kComponent, info.manifest,
                           flags, error);
}

// static
void ComponentLoader::EnableBackgroundExtensionsForTesting() {
  g_enable_background_extensions_during_testing = true;
}

#if BUILDFLAG(IS_CHROMEOS) && BUILDFLAG(GOOGLE_CHROME_BRANDING)
// static
void ComponentLoader::DisableHelpAppForTesting() {
  g_enable_help_app = false;
}
#endif

void ComponentLoader::AddDefaultComponentExtensions(
    bool skip_session_components) {
  // Do not add component extensions that have background pages here -- add them
  // to AddDefaultComponentExtensionsWithBackgroundPages.
#if BUILDFLAG(IS_CHROMEOS)

#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
  if (g_enable_help_app) {
    Add(IDR_HELP_MANIFEST, base::FilePath(FILE_PATH_LITERAL(
                               "/usr/share/chromeos-assets/helpapp")));
  }
#endif  // BUILDFLAG(GOOGLE_CHROME_BRANDING)

  AddKeyboardApp();
#else   // BUILDFLAG(IS_CHROMEOS)
  DCHECK(!skip_session_components);
#endif  // BUILDFLAG(IS_CHROMEOS)

  if (!skip_session_components) {
    AddWebStoreApp();
#if BUILDFLAG(IS_CHROMEOS)
    AddChromeApp();
#endif  // BUILDFLAG(IS_CHROMEOS)
#if BUILDFLAG(ENABLE_PDF)
    Add(pdf_extension_util::GetManifest(),
        base::FilePath(FILE_PATH_LITERAL("pdf")));
#endif  // BUILDFLAG(ENABLE_PDF)
  }

  AddDefaultComponentExtensionsWithBackgroundPages(skip_session_components);
}

void ComponentLoader::AddDefaultComponentExtensionsForKioskMode(
    bool skip_session_components) {
  // Do not add component extensions that have background pages here -- add them
  // to AddDefaultComponentExtensionsWithBackgroundPagesForKioskMode.

  // No component extension for kiosk app launch splash screen.
  if (skip_session_components) {
    return;
  }

#if BUILDFLAG(IS_CHROMEOS)
  // Add virtual keyboard.
  AddKeyboardApp();
#endif  // BUILDFLAG(IS_CHROMEOS)

  AddDefaultComponentExtensionsWithBackgroundPagesForKioskMode();

#if BUILDFLAG(ENABLE_PDF)
  Add(pdf_extension_util::GetManifest(),
      base::FilePath(FILE_PATH_LITERAL("pdf")));
#endif
}

void ComponentLoader::AddDefaultComponentExtensionsWithBackgroundPages(
    bool skip_session_components) {
  const base::CommandLine* command_line =
      base::CommandLine::ForCurrentProcess();

  // Component extensions with background pages are not enabled during tests
  // because they generate a lot of background behavior that can interfere.
  const bool should_disable_background_extensions =
      !g_enable_background_extensions_during_testing &&
      (command_line->HasSwitch(::switches::kTestType) ||
       command_line->HasSwitch(
           ::switches::kDisableComponentExtensionsWithBackgroundPages));

#if BUILDFLAG(ENABLE_HANGOUT_SERVICES_EXTENSION)
  const bool enable_hangout_services_extension_for_testing =
      command_line->HasSwitch(::switches::kTestType) &&
      command_line->HasSwitch(
          ::switches::kEnableHangoutServicesExtensionForTesting);
  if (!skip_session_components &&
      (!should_disable_background_extensions ||
       enable_hangout_services_extension_for_testing)) {
    AddHangoutServicesExtension();
  }
#endif  // BUILDFLAG(ENABLE_HANGOUT_SERVICES_EXTENSION)

  if (should_disable_background_extensions) {
    return;
  }

  if (!skip_session_components) {
#if BUILDFLAG(IS_CHROMEOS)
    Add(IDR_ECHO_MANIFEST,
        base::FilePath(FILE_PATH_LITERAL("/usr/share/chromeos-assets/echo")));
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
    AddComponentFromDirWithManifestFilename(
        base::FilePath("/usr/share/chromeos-assets/quickoffice"),
        extension_misc::kQuickOfficeComponentExtensionId,
        extensions::kManifestFilename, extensions::kManifestFilename, {}, {});
#endif  // BUILDFLAG(GOOGLE_CHROME_BRANDING)
#endif  // BUILDFLAG(IS_CHROMEOS)

#if BUILDFLAG(IS_CHROMEOS)
    if (command_line->HasSwitch(switches::kLoadGuestModeTestExtension)) {
      base::FilePath path = base::FilePath(command_line->GetSwitchValueASCII(
          switches::kLoadGuestModeTestExtension));
      AddGuestModeTestExtension(path);
    }
    AddImageLoaderExtension();

#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
    // TODO(https://crbug.com/1005083): Force the off the record profile to be
    // created to allow the virtual keyboard to work in guest mode.
    if (!IsNormalSession()) {
      ExtensionsBrowserClient::Get()->GetOffTheRecordContext(profile_);
    }
#endif  // BUILDFLAG(GOOGLE_CHROME_BRANDING)

    Add(IDR_ARC_SUPPORT_MANIFEST,
        base::FilePath(FILE_PATH_LITERAL("chromeos/arc_support")));
#endif  // BUILDFLAG(IS_CHROMEOS)
  }

  AddAimEligibilityExtension();
  AddGlicExtension();
  AddContextualTasksExtension();

// http://crbug.com/41070702
#if BUILDFLAG(GOOGLE_CHROME_BRANDING) && !BUILDFLAG(IS_CHROMEOS)
  AddNetworkSpeechSynthesisExtension();
#endif  // BUILDFLAG(GOOGLE_CHROME_BRANDING) && !BUILDFLAG(IS_CHROMEOS)
}

void ComponentLoader::
    AddDefaultComponentExtensionsWithBackgroundPagesForKioskMode() {
  const base::CommandLine* command_line =
      base::CommandLine::ForCurrentProcess();

  // Component extensions with background pages are not enabled during tests
  // because they generate a lot of background behavior that can interfere.
  if (!g_enable_background_extensions_during_testing &&
      (command_line->HasSwitch(::switches::kTestType) ||
       command_line->HasSwitch(
           ::switches::kDisableComponentExtensionsWithBackgroundPages))) {
    return;
  }

#if BUILDFLAG(ENABLE_HANGOUT_SERVICES_EXTENSION)
  AddHangoutServicesExtension();
#endif  // BUILDFLAG(ENABLE_HANGOUT_SERVICES_EXTENSION)
}

void ComponentLoader::UnloadComponent(ComponentExtensionInfo* component) {
  if (extension_system_->is_ready()) {
    auto* registrar = ExtensionRegistrar::Get(profile_);
    registrar->RemoveComponentExtension(component->extension_id);
  }
}

#if BUILDFLAG(IS_CHROMEOS)
bool ComponentLoader::IsPendingAdd(const ExtensionId& extension_id) const {
  return pending_extension_ids_.contains(extension_id);
}

bool ComponentLoader::ExistsOrPendingAdd(
    const ExtensionId& extension_id) const {
  return Exists(extension_id) || IsPendingAdd(extension_id);
}

void ComponentLoader::AddComponentFromDirWithManifestFilename(
    const base::FilePath& root_directory,
    const ExtensionId& extension_id,
    const base::FilePath::CharType* manifest_file_name,
    const base::FilePath::CharType* guest_manifest_file_name,
    base::OnceClosure done_cb,
    base::OnceClosure error_cb) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  CHECK(!IsPendingAdd(extension_id));
  pending_extension_ids_.emplace(extension_id);

  const base::FilePath::CharType* manifest_filename =
      IsNormalSession() ? manifest_file_name : guest_manifest_file_name;

  GetExtensionFileTaskRunner()->PostTaskAndReplyWithResult(
      FROM_HERE,
      base::BindOnce(&LoadManifestOnFileThread, root_directory,
                     manifest_filename, true),
      base::BindOnce(&ComponentLoader::FinishAddComponentFromDir,
                     weak_factory_.GetWeakPtr(), root_directory, extension_id,
                     std::nullopt, std::nullopt, std::move(done_cb),
                     std::move(error_cb)));
}

void ComponentLoader::FinishAddComponentFromDir(
    const base::FilePath& root_directory,
    const ExtensionId& extension_id,
    const std::optional<std::string>& name_string,
    const std::optional<std::string>& description_string,
    base::OnceClosure done_cb,
    base::OnceClosure error_cb,
    std::optional<base::DictValue> manifest) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  // Extension is removed during loading. Skip adding in this case.
  if (!IsPendingAdd(extension_id)) {
    if (error_cb) {
      std::move(error_cb).Run();
    }
    return;
  }
  pending_extension_ids_.erase(extension_id);

  if (!manifest) {
    if (error_cb) {
      std::move(error_cb).Run();
    }
    return;  // Error already logged.
  }

  if (name_string) {
    manifest->Set(manifest_keys::kName, name_string.value());
  }

  if (description_string) {
    manifest->Set(manifest_keys::kDescription, description_string.value());
  }

  std::string actual_extension_id =
      Add(std::move(*manifest), root_directory, false);
  CHECK_EQ(extension_id, actual_extension_id);
  if (done_cb) {
    std::move(done_cb).Run();
  }
}

void ComponentLoader::AddComponentFromDir(const base::FilePath& root_directory,
                                          const ExtensionId& extension_id,
                                          base::OnceClosure done_cb) {
  AddComponentFromDirWithManifestFilename(
      root_directory, extension_id, extensions::kManifestFilename,
      extension_misc::kGuestManifestFilename, std::move(done_cb), {});
}

void ComponentLoader::AddChromeOsSpeechSynthesisExtensions() {
  if (!ExistsOrPendingAdd(extension_misc::kGoogleSpeechSynthesisExtensionId)) {
    AddComponentFromDir(
        ::features::IsAccessibilityManifestV3EnabledForGoogleTts()
            ? base::FilePath(
                  extension_misc::kGoogleSpeechSynthesisManifestV3ExtensionPath)
            : base::FilePath(
                  extension_misc::kGoogleSpeechSynthesisExtensionPath),
        extension_misc::kGoogleSpeechSynthesisExtensionId,
        base::BindRepeating(
            &ComponentLoader::FinishLoadSpeechSynthesisExtension,
            weak_factory_.GetWeakPtr(),
            extension_misc::kGoogleSpeechSynthesisExtensionId));
  }

  if (!ExistsOrPendingAdd(extension_misc::kEspeakSpeechSynthesisExtensionId)) {
    AddComponentFromDir(
        base::FilePath(
            ::features::IsAccessibilityManifestV3EnabledForEspeakNGTts()
                ? extension_misc::kEspeakManifestV3SpeechSynthesisExtensionPath
                : extension_misc::kEspeakSpeechSynthesisExtensionPath),
        extension_misc::kEspeakSpeechSynthesisExtensionId,
        base::BindRepeating(
            &ComponentLoader::FinishLoadSpeechSynthesisExtension,
            weak_factory_.GetWeakPtr(),
            extension_misc::kEspeakSpeechSynthesisExtensionId));
  }
}

void ComponentLoader::FinishLoadSpeechSynthesisExtension(
    const ExtensionId& extension_id) {
  // TODO(crbug.com/41449926): mitigation for extension not awake after
  // load.
  extensions::ProcessManager::Get(profile_)->WakeEventPage(extension_id,
                                                           base::DoNothing());
}
#endif  // BUILDFLAG(IS_CHROMEOS)

}  // namespace extensions
