// 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/supervised_user/supervised_user_extensions_manager.h"

#include <algorithm>
#include <optional>
#include <string>

#include "base/metrics/histogram_functions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/extension_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_key.h"
#include "chrome/browser/supervised_user/supervised_user_browser_utils.h"
#include "chrome/browser/supervised_user/supervised_user_extensions_metrics_recorder.h"
#include "chrome/browser/supervised_user/supervised_user_service_factory.h"
#include "chrome/common/pref_names.h"
#include "chrome/grit/generated_resources.h"
#include "components/supervised_user/core/browser/supervised_user_preferences.h"
#include "components/supervised_user/core/browser/supervised_user_service.h"
#include "components/supervised_user/core/common/features.h"
#include "components/supervised_user/core/common/pref_names.h"
#include "components/supervised_user/core/common/supervised_user_constants.h"
#include "extensions/browser/disable_reason.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_registrar.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/pref_names.h"
#include "ui/base/l10n/l10n_util.h"

namespace extensions {

namespace {

// These extensions are allowed for supervised users for internal development
// purposes.
constexpr char const* kAllowlistExtensionIds[] = {
    "behllobkkfkfnphdnhnkndlbkcpglgmj"  // Tast extension.
};

// Returns the set of extensions that are missing parent approval.
base::DictValue GetExtensionsMissingApproval(const PrefService& user_prefs) {
  const base::DictValue& user_extensions_dict =
      user_prefs.GetDict(pref_names::kExtensions);
  const base::DictValue& approved_extensions_dict =
      user_prefs.GetDict(prefs::kSupervisedUserApprovedExtensions);
  base::DictValue unapproved_extensions_dict;

  // Deduce which extensions are not parent-approved based on the
  // corresponding preferences, as at the time of creation of
  // `SupervisedUserExtensionsManager`, the extensions are not yet loaded in
  // the registry.
  for (auto extension_entry : user_extensions_dict) {
    if (!approved_extensions_dict.contains(extension_entry.first)) {
      unapproved_extensions_dict.Set(extension_entry.first, true);
    }
  }
  return unapproved_extensions_dict;
}
}  // namespace

SupervisedUserExtensionsManager::SupervisedUserExtensionsManager(
    content::BrowserContext* context)
    : context_(context),
      extension_prefs_(ExtensionPrefs::Get(static_cast<Profile*>(context))),
      extension_system_(ExtensionSystem::Get(static_cast<Profile*>(context))),
      extension_registry_(
          ExtensionRegistry::Get(static_cast<Profile*>(context))),
      user_prefs_(static_cast<Profile*>(context)->GetPrefs()) {
  registry_observation_.Observe(extension_registry_);
  pref_change_registrar_.Init(user_prefs_);
  pref_change_registrar_.Add(
      prefs::kSupervisedUserId,
      base::BindRepeating(&SupervisedUserExtensionsManager::
                              ActivateManagementPolicyAndUpdateRegistration,
                          base::Unretained(this)));
  pref_change_registrar_.Add(
      prefs::kSupervisedUserApprovedExtensions,
      base::BindRepeating(
          &SupervisedUserExtensionsManager::RefreshApprovedExtensionsFromPrefs,
          base::Unretained(this)));
  pref_change_registrar_.Add(
      prefs::kSkipParentApprovalToInstallExtensions,
      base::BindRepeating(&SupervisedUserExtensionsManager::
                              OnSkipParentApprovalToInstallExtensionsChanged,
                          base::Unretained(this)));

  RefreshApprovedExtensionsFromPrefs();
  ActivateManagementPolicyAndUpdateRegistration();
}

SupervisedUserExtensionsManager::~SupervisedUserExtensionsManager() {
  pref_change_registrar_.RemoveAll();

  extensions::ManagementPolicy* management_policy =
      extension_system_->management_policy();
  if (management_policy && is_active_policy_for_supervised_users_) {
    management_policy->UnregisterProvider(this);
  }
}

void SupervisedUserExtensionsManager::UpdateManagementPolicyRegistration() {
  extensions::ManagementPolicy* management_policy =
      extension_system_->management_policy();
  if (management_policy) {
    if (is_active_policy_for_supervised_users_) {
      management_policy->RegisterProvider(this);
    } else {
      management_policy->UnregisterProvider(this);
    }
    // Re-check the policy to make sure any new settings get applied.
    extension_system_->extension_service()->CheckManagementPolicy();
  }
}

void SupervisedUserExtensionsManager::AddExtensionApproval(
    const extensions::Extension& extension) {
  if (!is_active_policy_for_supervised_users_) {
    return;
  }
  if (!approved_extensions_set_.contains(extension.id())) {
    UpdateApprovedExtension(extension.id(), extension.VersionString(),
                            ApprovedExtensionChange::kAdd);
  }
}

void SupervisedUserExtensionsManager::MaybeRecordPermissionsIncreaseMetrics(
    const extensions::Extension& extension) {
  if (!is_active_policy_for_supervised_users_) {
    return;
  }
  if (extension_prefs_->DidExtensionEscalatePermissions(extension.id())) {
    SupervisedUserExtensionsMetricsRecorder::RecordExtensionsUmaMetrics(
        SupervisedUserExtensionsMetricsRecorder::UmaExtensionState::
            kPermissionsIncreaseGranted);
  }
}

void SupervisedUserExtensionsManager::RemoveExtensionApproval(
    const extensions::Extension& extension) {
  if (!is_active_policy_for_supervised_users_) {
    return;
  }
  if (approved_extensions_set_.contains(extension.id())) {
    UpdateApprovedExtension(extension.id(), extension.VersionString(),
                            ApprovedExtensionChange::kRemove);
  }
}

bool SupervisedUserExtensionsManager::IsExtensionAllowed(
    const extensions::Extension& extension) const {
  return GetExtensionState(extension) ==
         SupervisedUserExtensionsManager::ExtensionState::kAllowed;
}

bool SupervisedUserExtensionsManager::CanInstallExtensions() const {
  supervised_user::SupervisedUserService* supervised_user_service =
      supervised_user::SupervisedUserServiceFactory::GetForProfile(
          Profile::FromBrowserContext(context_));
  bool has_custodian = supervised_user_service->GetCustodian() ||
                       supervised_user_service->GetSecondCustodian();
    return has_custodian;
}

void SupervisedUserExtensionsManager::RecordExtensionEnablementUmaMetrics(
    bool enabled) const {
  if (!is_active_policy_for_supervised_users_) {
    return;
  }
  auto state =
      enabled
          ? SupervisedUserExtensionsMetricsRecorder::EnablementState::kEnabled
          : SupervisedUserExtensionsMetricsRecorder::EnablementState::kDisabled;
  SupervisedUserExtensionsMetricsRecorder::RecordEnablementUmaMetrics(state);
}

std::string SupervisedUserExtensionsManager::GetDebugPolicyProviderName()
    const {
  // Save the string space in official builds.
#if DCHECK_IS_ON()
  return "Supervised User Service";
#else
  base::ImmediateCrash();
#endif
}

bool SupervisedUserExtensionsManager::UserMayLoad(
    const extensions::Extension* extension,
    std::u16string* error) const {
  ExtensionState result = GetExtensionState(*extension);
  bool may_load = result != ExtensionState::kBlocked;
  if (!may_load && error) {
    *error = GetExtensionsLockedMessage();
  }
  return may_load;
}

bool SupervisedUserExtensionsManager::MustRemainDisabled(
    const extensions::Extension* extension,
    extensions::disable_reason::DisableReason* reason) const {
  ExtensionState state = GetExtensionState(*extension);
  // Only extensions that require approval should be disabled.
  // Blocked extensions should be not loaded at all, and are taken care of
  // at UserMayLoad.
  bool must_remain_disabled = state == ExtensionState::kRequireApproval;

  if (!must_remain_disabled) {
    return false;
  }
  if (reason) {
    *reason = extensions::disable_reason::DISABLE_CUSTODIAN_APPROVAL_REQUIRED;
  }
  return true;
}

void SupervisedUserExtensionsManager::OnExtensionInstalled(
    content::BrowserContext* browser_context,
    const extensions::Extension* extension,
    bool is_update) {
  if (!is_update) {
    if (!is_active_policy_for_supervised_users_) {
      return;
    }
    // At the end of an extension installation under the mode where
    // the child can skip parental approval, we grant the extension
    // the parental approval.
    // Applies to all installations (via the Webstore in the same
    // client and for extensions received through sync).
    const Profile* profile = Profile::FromBrowserContext(browser_context);
    if (!supervised_user::SupervisedUserCanSkipExtensionParentApprovals(
            profile)) {
      return;
    }
    CHECK(extension);
    if (!approved_extensions_set_.contains(extension->id())) {
      AddExtensionApproval(*extension);
    }
  }

  // A change in extension state might be required upon extension update
  // or upon granting a new approval.
  ChangeExtensionStateIfNecessary(extension->id());
}

void SupervisedUserExtensionsManager::OnExtensionUninstalled(
    content::BrowserContext* browser_context,
    const extensions::Extension* extension,
    extensions::UninstallReason reason) {
  if (!is_active_policy_for_supervised_users_) {
    return;
  }
  if (approved_extensions_set_.contains(extension->id())) {
    UpdateApprovedExtension(extension->id(), extension->VersionString(),
                            ApprovedExtensionChange::kRemove);
  }
  if (IsLocallyParentApprovedExtension(extension->id())) {
    RemoveLocalParentalApproval(/*extension_ids=*/{extension->id()});
  }
}

SupervisedUserExtensionsManager::ExtensionState
SupervisedUserExtensionsManager::GetExtensionState(
    const extensions::Extension& extension) const {
  bool was_installed_by_default = extension.was_installed_by_default();
#if BUILDFLAG(IS_CHROMEOS)
  // On Chrome OS all external sources are controlled by us so it means that
  // they are "default". Method was_installed_by_default returns false because
  // extensions creation flags are ignored in case of default extensions with
  // update URL(the flags aren't passed to OnExternalExtensionUpdateUrlFound).
  // TODO(dpolukhin): remove this Chrome OS specific code as soon as creation
  // flags are not ignored.
  was_installed_by_default =
      extensions::Manifest::IsExternalLocation(extension.location());
#endif

  // Note: Component extensions are protected from modification/uninstallation
  // anyway, so there's no need to enforce them again for supervised users.
  // Also, leave policy-installed extensions alone - they have their own
  // management; in particular we don't want to override the force-install list.
  if (extensions::Manifest::IsComponentLocation(extension.location()) ||
      extensions::Manifest::IsPolicyLocation(extension.location()) ||
      extension.is_theme() || extension.is_shared_module() ||
      was_installed_by_default) {
    return SupervisedUserExtensionsManager::ExtensionState::kAllowed;
  }

  if (std::ranges::contains(kAllowlistExtensionIds, extension.id())) {
    return SupervisedUserExtensionsManager::ExtensionState::kAllowed;
  }

  if (ShouldBlockExtension(extension.id())) {
    return SupervisedUserExtensionsManager::ExtensionState::kBlocked;
  }

  if (approved_extensions_set_.contains(extension.id())) {
    return SupervisedUserExtensionsManager::ExtensionState::kAllowed;
  }
  if (IsLocallyParentApprovedExtension(extension.id())) {
    return SupervisedUserExtensionsManager::ExtensionState::kAllowed;
  }
  return SupervisedUserExtensionsManager::ExtensionState::kRequireApproval;
}

void SupervisedUserExtensionsManager::RefreshApprovedExtensionsFromPrefs() {
  // Keep track of currently approved extensions. We need to disable them if
  // they are not in the approved set anymore.
  std::set<std::string> extensions_to_be_checked(
      std::move(approved_extensions_set_));

  // The purpose here is to re-populate the approved_extensions_set_, which is
  // used in GetExtensionState() to keep track of approved extensions.
  approved_extensions_set_.clear();

  // TODO(crbug.com/40685974): This dict is actually just a set. The extension
  // version information stored in the values is unnecessary. It is only there
  // for backwards compatibility. Remove the version information once sufficient
  // users have migrated away from M83.
  for (auto it :
       user_prefs_->GetDict(prefs::kSupervisedUserApprovedExtensions)) {
    approved_extensions_set_.insert(it.first);
    extensions_to_be_checked.insert(it.first);
  }

  // If an extension goes through the parent approval flow in another client
  // with extension parental controls in place, we remove it in this client from
  // the locally  parent-approval set.
  RemoveLocalParentalApproval(approved_extensions_set_);

  for (const auto& extension_id : extensions_to_be_checked) {
    ChangeExtensionStateIfNecessary(extension_id);
  }
}

void SupervisedUserExtensionsManager::SetActiveForSupervisedUsers() {
  auto* profile = Profile::FromBrowserContext(context_);
  is_active_policy_for_supervised_users_ =
      profile && supervised_user::AreExtensionsPermissionsEnabled(profile);
}

void SupervisedUserExtensionsManager::
    ActivateManagementPolicyAndUpdateRegistration() {
  SetActiveForSupervisedUsers();
  UpdateManagementPolicyRegistration();
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
  MaybeMarkExtensionsLocallyParentApproved();
#endif  // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
}

// TODO(crbug.com/40685974): We don't need the extension version information.
// It's only included for backwards compatibility with previous versions of
// Chrome. Remove the version information once a sufficient number of users have
// migrated away from M83.
void SupervisedUserExtensionsManager::UpdateApprovedExtension(
    const std::string& extension_id,
    const std::string& version,
    ApprovedExtensionChange type) {
  ScopedDictPrefUpdate update(user_prefs_,
                              prefs::kSupervisedUserApprovedExtensions);
  base::DictValue& approved_extensions = update.Get();
  bool success = false;
  const Profile* profile = Profile::FromBrowserContext(context_);
  switch (type) {
    case ApprovedExtensionChange::kAdd:
      CHECK(!approved_extensions.FindString(extension_id));
      approved_extensions.Set(extension_id, std::move(version));

      SupervisedUserExtensionsMetricsRecorder::RecordExtensionsUmaMetrics(
          supervised_user::SupervisedUserCanSkipExtensionParentApprovals(
              profile)
              ? SupervisedUserExtensionsMetricsRecorder::UmaExtensionState::
                    kApprovalGrantedByDefault
              : SupervisedUserExtensionsMetricsRecorder::UmaExtensionState::
                    kApprovalGranted);
      break;
    case ApprovedExtensionChange::kRemove:
      success = approved_extensions.Remove(extension_id);
      CHECK(success);
      SupervisedUserExtensionsMetricsRecorder::RecordExtensionsUmaMetrics(
          SupervisedUserExtensionsMetricsRecorder::UmaExtensionState::
              kApprovalRemoved);
      break;
  }
}

std::u16string SupervisedUserExtensionsManager::GetExtensionsLockedMessage()
    const {
  supervised_user::SupervisedUserService* supervised_user_service =
      supervised_user::SupervisedUserServiceFactory::GetForProfile(
          Profile::FromBrowserContext(context_));
  std::optional<supervised_user::Custodian> custodian =
      supervised_user_service->GetCustodian();
  return l10n_util::GetStringFUTF16(
      IDS_EXTENSIONS_LOCKED_SUPERVISED_USER,
      base::UTF8ToUTF16(custodian ? custodian->GetName() : ""));
}

void SupervisedUserExtensionsManager::ChangeExtensionStateIfNecessary(
    const std::string& extension_id) {
  // If the profile is not supervised, do nothing.
  // TODO(crbug/1026900): SupervisedUserService should not be active if the
  // profile is not even supervised during browser tests, i.e. this check
  // shouldn't be needed.
  if (!is_active_policy_for_supervised_users_) {
    return;
  }
  const Extension* extension =
      extension_registry_->GetInstalledExtension(extension_id);
  // If the extension is not installed (yet), do nothing.
  // Things will be handled after installation.
  if (!extension) {
    return;
  }

  auto* registrar = extensions::ExtensionRegistrar::Get(context_);
  ExtensionState state = GetExtensionState(*extension);
  switch (state) {
    // BLOCKED extensions should be already disabled and we don't need to change
    // their state here.
    case ExtensionState::kBlocked:
      break;
    case ExtensionState::kRequireApproval:
      registrar->DisableExtension(
          extension_id,
          {extensions::disable_reason::DISABLE_CUSTODIAN_APPROVAL_REQUIRED});
      break;
    case ExtensionState::kAllowed:
      extension_prefs_->RemoveDisableReason(
          extension_id,
          extensions::disable_reason::DISABLE_CUSTODIAN_APPROVAL_REQUIRED);
      // If not disabled for other reasons, enable it.
      if (extension_prefs_->GetDisableReasons(extension_id).empty()) {
        registrar->EnableExtension(extension_id);
      }
      break;
  }
}

bool SupervisedUserExtensionsManager::ShouldBlockExtension(
    const std::string& extension_id) const {
  // Users can always install extensions.
  return false;
}

#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
void SupervisedUserExtensionsManager::
    MaybeMarkExtensionsLocallyParentApproved() {
  supervised_user::LocallyParentApprovedExtensionsMigrationState
      migration_state = static_cast<
          supervised_user::LocallyParentApprovedExtensionsMigrationState>(
          user_prefs_->GetInteger(
              prefs::kLocallyParentApprovedExtensionsMigrationState));
  if (migration_state ==
      supervised_user::LocallyParentApprovedExtensionsMigrationState::
          kComplete) {
    return;
  }

  if (supervised_user::IsSubjectToParentalControls(*user_prefs_)) {
    // In the case of of a supervised user locally approve their extensions.
    DoExtensionsMigrationToParentApproved();
  }

  // Always mark the migration done on feature release for the currently used
  // profile. Applies to both for supervised and regular users. This way, if the
  // profile is later Gellerized or if a supervised user takes over an existing
  // unsupervised profile, their extensions will not be locally approved,
  // instead they should remain in pending approval state.
  user_prefs_->SetInteger(
      prefs::kLocallyParentApprovedExtensionsMigrationState,
      static_cast<int>(
          supervised_user::LocallyParentApprovedExtensionsMigrationState::
              kComplete));
}

void SupervisedUserExtensionsManager::DoExtensionsMigrationToParentApproved() {
  base::DictValue unapproved_extensions_dict =
      GetExtensionsMissingApproval(*user_prefs_);
  user_prefs_->SetDict(prefs::kSupervisedUserLocallyParentApprovedExtensions,
                       std::move(unapproved_extensions_dict));

  auto& approved_extensions_dict = user_prefs_->GetDict(
      prefs::kSupervisedUserLocallyParentApprovedExtensions);
  for (auto extension_entry : approved_extensions_dict) {
    if (extension_registry_->GetInstalledExtension(extension_entry.first)) {
      ChangeExtensionStateIfNecessary(extension_entry.first);
    }
    SupervisedUserExtensionsMetricsRecorder::RecordExtensionsUmaMetrics(
        SupervisedUserExtensionsMetricsRecorder::UmaExtensionState::
            kLocalApprovalGranted);
  }
}
#endif  // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)

bool SupervisedUserExtensionsManager::IsLocallyParentApprovedExtension(
    const std::string& extension_id) const {
  const base::DictValue& current_locally_approved_dict = user_prefs_->GetDict(
      prefs::kSupervisedUserLocallyParentApprovedExtensions);
  return current_locally_approved_dict.contains(extension_id);
}

void SupervisedUserExtensionsManager::RemoveLocalParentalApproval(
    const std::set<std::string>& extension_ids) {
  base::DictValue locally_approved_extensions_dict =
      user_prefs_
          ->GetDict(prefs::kSupervisedUserLocallyParentApprovedExtensions)
          .Clone();
  for (const auto& extension_id : extension_ids) {
    locally_approved_extensions_dict.Remove(extension_id);
  }
  user_prefs_->SetDict(prefs::kSupervisedUserLocallyParentApprovedExtensions,
                       std::move(locally_approved_extensions_dict));
}

void SupervisedUserExtensionsManager::
    OnSkipParentApprovalToInstallExtensionsChanged() {
  const Profile* profile = Profile::FromBrowserContext(context_);
  if (!is_active_policy_for_supervised_users_ ||
      !supervised_user::SupervisedUserCanSkipExtensionParentApprovals(
          profile)) {
    return;
  }

  auto unapproved_extensions_dict =
      GetExtensionsMissingApproval(*user_prefs_.get());
  for (auto extension_entry : unapproved_extensions_dict) {
    const Extension* extension =
        extension_registry_->GetInstalledExtension(extension_entry.first);
    if (extension) {
      ExtensionState state = GetExtensionState(*extension);
      if (state == ExtensionState::kRequireApproval ||
          (state == ExtensionState::kAllowed &&
           IsLocallyParentApprovedExtension(extension->id()))) {
        AddExtensionApproval(*extension);
      }
      // If the extension id from the preferences has not been installed yet,
      // the approval will be granted at the end of installation.
      // See `OnExtensionInstalled`.
    }
  }
}

}  // namespace extensions
