// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "extensions/browser/user_script_world_configuration_manager.h"

#include "components/keyed_service/content/browser_context_dependency_manager.h"
#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_prefs_factory.h"
#include "extensions/browser/extension_registry_factory.h"
#include "extensions/browser/extensions_browser_client.h"
#include "extensions/browser/renderer_startup_helper.h"

namespace extensions {

namespace {

constexpr char kDefaultUserScriptWorldKey[] = "_default";
constexpr char kUserScriptWorldMessagingKey[] = "messaging";
constexpr char kUserScriptWorldCspKey[] = "csp";

class UserScriptWorldConfigurationManagerFactory
    : public BrowserContextKeyedServiceFactory {
 public:
  UserScriptWorldConfigurationManagerFactory()
      : BrowserContextKeyedServiceFactory(
            "UserScriptWorldConfigurationManager",
            BrowserContextDependencyManager::GetInstance()) {
    DependsOn(ExtensionPrefsFactory::GetInstance());
    DependsOn(ExtensionRegistryFactory::GetInstance());
    DependsOn(RendererStartupHelperFactory::GetInstance());
  }

  UserScriptWorldConfigurationManagerFactory(
      const UserScriptWorldConfigurationManagerFactory&) = delete;
  UserScriptWorldConfigurationManagerFactory& operator=(
      const UserScriptWorldConfigurationManagerFactory&) = delete;
  ~UserScriptWorldConfigurationManagerFactory() override = default;

  UserScriptWorldConfigurationManager* GetForBrowserContext(
      content::BrowserContext* context) {
    return static_cast<UserScriptWorldConfigurationManager*>(
        GetServiceForBrowserContext(context, /*create=*/true));
  }

 private:
  // BrowserContextKeyedServiceFactory:
  content::BrowserContext* GetBrowserContextToUse(
      content::BrowserContext* context) const override {
    // TODO(devlin): I wonder if it would make sense for this to have its own
    // instance in incognito. That would allow split-mode extensions to have
    // incognito-only world specifications and have them cleaned up when the
    // profile is destroyed.
    return ExtensionsBrowserClient::Get()->GetContextRedirectedToOriginal(
        context);
  }
  std::unique_ptr<KeyedService> BuildServiceInstanceForBrowserContext(
      content::BrowserContext* context) const override {
    return std::make_unique<UserScriptWorldConfigurationManager>(context);
  }
};

// Returns the key entry in the user script world dictionary to use for the
// provided `world_id`.
std::string_view GetUserScriptWorldKeyForWorldId(
    const std::optional<std::string>& world_id) {
  return world_id ? world_id->c_str() : kDefaultUserScriptWorldKey;
}

// Attempts to parse the given `dict` into a mojom::UserScriptWorldInfo. If
// `dict` is null, returns a default specification for a
// mojom::UserScriptWorldInfo.
mojom::UserScriptWorldInfoPtr ParseUserScriptWorldInfo(
    const ExtensionId& extension_id,
    const std::optional<std::string>& world_id,
    const base::DictValue* dict) {
  bool enable_messaging = false;
  std::optional<std::string> csp;
  if (dict) {
    enable_messaging =
        dict->FindBool(kUserScriptWorldMessagingKey).value_or(false);
    const std::string* csp_pref = dict->FindString(kUserScriptWorldCspKey);
    csp = csp_pref ? std::make_optional(*csp_pref) : std::nullopt;
  }

  return mojom::UserScriptWorldInfo::New(extension_id, world_id, csp,
                                         enable_messaging);
}

}  // namespace

UserScriptWorldConfigurationManager::UserScriptWorldConfigurationManager(
    content::BrowserContext* browser_context)
    : extension_prefs_(ExtensionPrefs::Get(browser_context)),
      renderer_helper_(
          RendererStartupHelperFactory::GetForBrowserContext(browser_context)) {
  registry_observation_.Observe(ExtensionRegistry::Get(browser_context));
}

UserScriptWorldConfigurationManager::~UserScriptWorldConfigurationManager() =
    default;

void UserScriptWorldConfigurationManager::SetUserScriptWorldInfo(
    const Extension& extension,
    mojom::UserScriptWorldInfoPtr world_info) {
  CHECK(!world_info->world_id || !world_info->world_id->empty());
  // Persist world configuratation in ExtensionPrefs.
  ExtensionPrefs::ScopedDictionaryUpdate update(
      extension_prefs_, extension.id(), kUserScriptsWorldsConfiguration.name);
  std::unique_ptr<prefs::DictionaryValueUpdate> update_dict = update.Get();
  if (!update_dict) {
    update_dict = update.Create();
  }

  base::DictValue world_info_dict;
  world_info_dict.Set(kUserScriptWorldMessagingKey,
                      world_info->enable_messaging);
  if (world_info->csp.has_value()) {
    world_info_dict.Set(kUserScriptWorldCspKey, *world_info->csp);
  }

  update_dict->SetKey(GetUserScriptWorldKeyForWorldId(world_info->world_id),
                      base::Value(std::move(world_info_dict)));

  renderer_helper_->SetUserScriptWorldProperties(extension,
                                                 std::move(world_info));
}

void UserScriptWorldConfigurationManager::ClearUserScriptWorldInfo(
    const Extension& extension,
    const std::optional<std::string>& world_id) {
  CHECK(!world_id || !world_id->empty());
  ExtensionPrefs::ScopedDictionaryUpdate update(
      extension_prefs_, extension.id(), kUserScriptsWorldsConfiguration.name);
  std::unique_ptr<prefs::DictionaryValueUpdate> update_dict = update.Get();

  if (!update_dict) {
    return;  // No configs.
  }

  std::string_view world_key = GetUserScriptWorldKeyForWorldId(world_id);
  if (!update_dict->HasKey(world_key)) {
    return;  // No config for this world ID.
  }

  update_dict->Remove(world_key);
  renderer_helper_->ClearUserScriptWorldProperties(extension, world_id);
}

mojom::UserScriptWorldInfoPtr
UserScriptWorldConfigurationManager::GetUserScriptWorldInfo(
    const ExtensionId& extension_id,
    const std::optional<std::string>& world_id) {
  CHECK(!world_id || !world_id->empty());
  const base::DictValue* worlds_configuration =
      extension_prefs_->ReadPrefAsDictionary(extension_id,
                                             kUserScriptsWorldsConfiguration);
  const base::DictValue* world_info =
      worlds_configuration ? worlds_configuration->FindDict(
                                 GetUserScriptWorldKeyForWorldId(world_id))
                           : nullptr;

  return ParseUserScriptWorldInfo(extension_id, world_id, world_info);
}

std::vector<mojom::UserScriptWorldInfoPtr>
UserScriptWorldConfigurationManager::GetAllUserScriptWorlds(
    const ExtensionId& extension_id) {
  const base::DictValue* worlds_configuration =
      extension_prefs_->ReadPrefAsDictionary(extension_id,
                                             kUserScriptsWorldsConfiguration);
  if (!worlds_configuration) {
    return {};
  }

  std::vector<mojom::UserScriptWorldInfoPtr> result;
  for (auto [world_id_key, world_value] : *worlds_configuration) {
    if (world_id_key.length() < 1) {
      continue;  // Invalid key. Ignore.
    }

    if (!world_value.is_dict()) {
      continue;  // Invalid value. Ignore.
    }

    std::optional<std::string> world_id;
    if (world_id_key[0] == '_') {
      if (world_id_key != kDefaultUserScriptWorldKey) {
        continue;  // Invalid key. Ignore.
      }
    } else {
      // Otherwise, the world ID is the key in the dictionary.
      world_id = world_id_key;
    }

    mojom::UserScriptWorldInfoPtr parsed_world = ParseUserScriptWorldInfo(
        extension_id, world_id, &world_value.GetDict());
    if (!parsed_world) {
      continue;  // Failed to parse. Ignore.
    }

    result.push_back(std::move(parsed_world));
  }

  return result;
}

// static
BrowserContextKeyedServiceFactory&
UserScriptWorldConfigurationManager::GetFactory() {
  static base::NoDestructor<UserScriptWorldConfigurationManagerFactory>
      g_factory;
  return *g_factory;
}

// static
UserScriptWorldConfigurationManager* UserScriptWorldConfigurationManager::Get(
    content::BrowserContext* browser_context) {
  auto& factory =
      static_cast<UserScriptWorldConfigurationManagerFactory&>(GetFactory());
  return factory.GetForBrowserContext(browser_context);
}

void UserScriptWorldConfigurationManager::OnExtensionWillBeInstalled(
    content::BrowserContext* browser_context,
    const Extension* extension,
    bool is_update,
    const std::string& old_name) {
  if (!is_update) {
    return;
  }

  extension_prefs_->UpdateExtensionPref(
      extension->id(), kUserScriptsWorldsConfiguration.name, std::nullopt);
}

}  // namespace extensions
