// Copyright 2018 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/policy/developer_tools_policy_handler.h"

#include <optional>

#include "base/command_line.h"
#include "base/strings/string_number_conversions.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/common/pref_names.h"
#include "components/policy/core/browser/policy_error_map.h"
#include "components/policy/core/common/policy_map.h"
#include "components/policy/policy_constants.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/pref_value_map.h"
#include "components/strings/grit/components_strings.h"

#if BUILDFLAG(IS_CHROMEOS)
#include "ash/constants/ash_switches.h"
#include "components/user_manager/user.h"
#include "components/user_manager/user_manager.h"
#endif  // BUILDFLAG(IS_CHROMEOS)

#if BUILDFLAG(ENABLE_EXTENSIONS_CORE)
#include "chrome/browser/policy/extension_developer_mode_policy_handler.h"
#endif  // BUILDFLAG(ENABLE_EXTENSIONS_CORE)

namespace policy {

namespace {

// The result of checking a policy value.
enum class PolicyCheckResult {
  // The policy is not set.
  kNotSet,
  // The policy is set to an invalid value.
  kInvalid,
  // The policy is set to a valid value.
  kValid
};

#if BUILDFLAG(IS_ANDROID)
// key::kDeveloperToolsDisabled has been deprecated and has never been supported
// on Android.
std::optional<DeveloperToolsAvailability> GetValueFromDeveloperToolsDisabledPolicy(
    const PolicyMap& policies) {
  return std::nullopt;
}
#else
// Checks the value of the DeveloperToolsDisabled policy. |errors| may be
// nullptr.
PolicyCheckResult CheckDeveloperToolsDisabled(
    const base::Value* developer_tools_disabled,
    policy::PolicyErrorMap* errors) {
  if (!developer_tools_disabled)
    return PolicyCheckResult::kNotSet;

  if (!developer_tools_disabled->is_bool()) {
    if (errors) {
      errors->AddError(key::kDeveloperToolsDisabled, IDS_POLICY_TYPE_ERROR,
                       base::Value::GetTypeName(base::Value::Type::BOOLEAN));
    }
    return PolicyCheckResult::kInvalid;
  }

  return PolicyCheckResult::kValid;
}

// Returns the target value of the |kDevToolsAvailability| pref derived only
// from the legacy DeveloperToolsDisabled policy. If this policy is not set or
// does not have a valid value, returns |nullopt|.
std::optional<DeveloperToolsAvailability> GetValueFromDeveloperToolsDisabledPolicy(
    const PolicyMap& policies) {
  const base::Value* developer_tools_disabled = policies.GetValue(
      key::kDeveloperToolsDisabled, base::Value::Type::BOOLEAN);

  if (CheckDeveloperToolsDisabled(developer_tools_disabled,
                                  nullptr /*error*/) !=
      PolicyCheckResult::kValid) {
    return std::nullopt;
  }

  return developer_tools_disabled->GetBool() ? DeveloperToolsAvailability::kDisallowed
                                             : DeveloperToolsAvailability::kAllowed;
}
#endif  // BUILDFLAG(IS_ANDROID)

// Returns true if |value| is within the valid range of the
// DeveloperToolsAvailability enum policy.
bool IsValidDeveloperToolsAvailabilityValue(int value) {
  return value >= 0 && value <= static_cast<int>(DeveloperToolsAvailability::kMaxValue);
}

// Checks the value of the DeveloperToolsAvailability policy. |errors| may be
// nullptr.
PolicyCheckResult CheckDeveloperToolsAvailability(
    const base::Value* developer_tools_availability,
    policy::PolicyErrorMap* errors) {
  if (!developer_tools_availability)
    return PolicyCheckResult::kNotSet;

  if (!developer_tools_availability->is_int()) {
    if (errors) {
      errors->AddError(key::kDeveloperToolsAvailability, IDS_POLICY_TYPE_ERROR,
                       base::Value::GetTypeName(base::Value::Type::INTEGER));
    }
    return PolicyCheckResult::kInvalid;
  }

  const int value = developer_tools_availability->GetInt();
  if (!IsValidDeveloperToolsAvailabilityValue(value)) {
    if (errors) {
      errors->AddError(key::kDeveloperToolsAvailability,
                       IDS_POLICY_OUT_OF_RANGE_ERROR,
                       base::NumberToString(value));
    }
    return PolicyCheckResult::kInvalid;
  }
  return PolicyCheckResult::kValid;
}

// Returns the target value of the |kDevToolsAvailability| pref derived only
// from the DeveloperToolsAvailability policy. If this policy is not set or does
// not have a valid value, returns |nullopt|.
std::optional<DeveloperToolsAvailability> GetValueFromDeveloperToolsAvailabilityPolicy(
    const PolicyMap& policies) {
  // It is safe to use `GetValueUnsafe()` because type checking is performed
  // before the value is used.
  const base::Value* developer_tools_availability =
      policies.GetValueUnsafe(key::kDeveloperToolsAvailability);

  if (CheckDeveloperToolsAvailability(developer_tools_availability,
                                      nullptr /*error*/) !=
      PolicyCheckResult::kValid) {
    return std::nullopt;
  }

  return static_cast<DeveloperToolsAvailability>(developer_tools_availability->GetInt());
}

// Returns the target value of the |kDevToolsAvailability| pref, derived from
// both the DeveloperToolsDisabled policy and the
// DeveloperToolsAvailability policy. If both policies are set,
// DeveloperToolsAvailability wins.
std::optional<DeveloperToolsAvailability> GetValueFromBothPolicies(
    const PolicyMap& policies) {
  const std::optional<DeveloperToolsAvailability> developer_tools_availability =
      GetValueFromDeveloperToolsAvailabilityPolicy(policies);

  if (developer_tools_availability.has_value()) {
    // DeveloperToolsAvailability overrides DeveloperToolsDisabled policy.
    return developer_tools_availability;
  }

  return GetValueFromDeveloperToolsDisabledPolicy(policies);
}

// Returns the current policy-set developer tools availability according to
// the values in |pref_service|. If no policy mandating developer tools
// availability is set, the default will be
// |DeveloperToolsAvailability::kDisallowedForForceInstalledExtensions|.
DeveloperToolsAvailability GetDevToolsAvailability(const PrefService* pref_sevice) {
  int value = pref_sevice->GetInteger(prefs::kDevToolsAvailability);
  if (!IsValidDeveloperToolsAvailabilityValue(value)) {
    // This should never happen, because the |kDevToolsAvailability| pref is
    // only set by DeveloperToolsPolicyHandler which validates the value range.
    // If it is not set, it will have its default value which is also valid, see
    // |RegisterProfilePrefs|.
    NOTREACHED();
  }

  return static_cast<DeveloperToolsAvailability>(value);
}

#if BUILDFLAG(IS_CHROMEOS)

// Returns true if developer tools availability is set by an active policy in
// |pref_service|.
bool IsDevToolsAvailabilitySetByPolicy(const PrefService* pref_service) {
  return pref_service->IsManagedPreference(prefs::kDevToolsAvailability);
}

// Returns the most restrictive availability within [|availability_1|,
// |availability_2|].
DeveloperToolsAvailability GetMostRestrictiveAvailability(DeveloperToolsAvailability availability_1,
                                            DeveloperToolsAvailability availability_2) {
  if (availability_1 == DeveloperToolsAvailability::kDisallowed ||
      availability_2 == DeveloperToolsAvailability::kDisallowed) {
    return DeveloperToolsAvailability::kDisallowed;
  }
  if (availability_1 == DeveloperToolsAvailability::kDisallowedForForceInstalledExtensions ||
      availability_2 == DeveloperToolsAvailability::kDisallowedForForceInstalledExtensions) {
    return DeveloperToolsAvailability::kDisallowedForForceInstalledExtensions;
  }
  return DeveloperToolsAvailability::kAllowed;
}

const PrefService* GetPrimaryUserPrefs() {
  auto* user_manager = user_manager::UserManager::Get();
  if (!user_manager) {
    return nullptr;
  }
  const user_manager::User* primary_user = user_manager->GetPrimaryUser();
  if (!primary_user) {
    return nullptr;
  }
  return primary_user->GetProfilePrefs();
}

#endif

}  // namespace

DeveloperToolsPolicyHandler::DeveloperToolsPolicyHandler() = default;

DeveloperToolsPolicyHandler::~DeveloperToolsPolicyHandler() = default;

bool DeveloperToolsPolicyHandler::CheckPolicySettings(
    const policy::PolicyMap& policies,
    policy::PolicyErrorMap* errors) {
  // It is safe to use `GetValueUnsafe()` because type checking is performed
  // before the value is used.
  // Enumerated policy DeveloperToolsAvailability.
  const base::Value* developer_tools_availability =
      policies.GetValueUnsafe(key::kDeveloperToolsAvailability);
  PolicyCheckResult developer_tools_availability_result =
      CheckDeveloperToolsAvailability(developer_tools_availability, errors);
  PolicyCheckResult developer_tools_disabled_result =
      PolicyCheckResult::kNotSet;
#if !BUILDFLAG(IS_ANDROID)
  // It is safe to use `GetValueUnsafe()` because type checking is performed
  // before the value is used.
  // Deprecated boolean policy DeveloperToolsDisabled.
  const base::Value* developer_tools_disabled =
      policies.GetValueUnsafe(key::kDeveloperToolsDisabled);
  developer_tools_disabled_result =
      CheckDeveloperToolsDisabled(developer_tools_disabled, errors);

  if (developer_tools_disabled_result == PolicyCheckResult::kValid &&
      developer_tools_availability_result == PolicyCheckResult::kValid) {
    errors->AddError(key::kDeveloperToolsDisabled, IDS_POLICY_OVERRIDDEN,
                     key::kDeveloperToolsAvailability);
  }
#endif  // !BUILDFLAG(IS_ANDROID)
  if (developer_tools_disabled_result != PolicyCheckResult::kValid &&
      developer_tools_availability_result != PolicyCheckResult::kValid) {
    return false;
  }

#if BUILDFLAG(ENABLE_EXTENSIONS_CORE)
  const std::optional<DeveloperToolsAvailability> policy = GetValueFromBothPolicies(policies);

  if (policy.has_value() && *policy == DeveloperToolsAvailability::kDisallowed &&
      extension_developer_mode_policy_handler_.IsValidPolicySet(policies)) {
    errors->AddError(key::kDeveloperToolsAvailability,
                     IDS_POLICY_DEVELOPER_TOOLS_EXTENSIONS_CONFLICT_MESSAGE,
                     key::kExtensionDeveloperModeSettings,
                     key::kDeveloperToolsAvailability,
                     /*error_path=*/{}, PolicyMap::MessageType::kInfo);
  }
#endif  // BUILDFLAG(ENABLE_EXTENSIONS_CORE)

  // Always continue to ApplyPolicySettings which can handle invalid policy
  // values.
  return true;
}

void DeveloperToolsPolicyHandler::ApplyPolicySettings(const PolicyMap& policies,
                                                      PrefValueMap* prefs) {
  const std::optional<DeveloperToolsAvailability> policy = GetValueFromBothPolicies(policies);

  if (policy.has_value()) {
    prefs->SetInteger(prefs::kDevToolsAvailability, static_cast<int>(*policy));

#if BUILDFLAG(ENABLE_EXTENSIONS_CORE)
    // ExtensionDeveloperModePolicySettings takes precedence over this policy.
    // Thus, we only set the value of kExtensionsUIDeveloperMode if the former
    // is not set.
    if (*policy == DeveloperToolsAvailability::kDisallowed &&
        !extension_developer_mode_policy_handler_.IsValidPolicySet(policies)) {
      prefs->SetValue(prefs::kExtensionsUIDeveloperMode, base::Value(false));
    }
#endif  // BUILDFLAG(ENABLE_EXTENSIONS_CORE)
  }
}

policy::DeveloperToolsAvailability
DeveloperToolsPolicyHandler::GetEffectiveAvailability(Profile* profile) {
#if BUILDFLAG(IS_CHROMEOS)
  base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
  if (command_line->HasSwitch(ash::switches::kForceDevToolsAvailable)) {
    return DeveloperToolsAvailability::kAllowed;
  }
#endif

  DeveloperToolsAvailability availability = GetDevToolsAvailability(profile->GetPrefs());
#if BUILDFLAG(IS_CHROMEOS)
  // Do not create DevTools if it's disabled for primary user.
  const PrefService* primary_user_prefs = GetPrimaryUserPrefs();
  if (primary_user_prefs &&
      IsDevToolsAvailabilitySetByPolicy(primary_user_prefs)) {
    availability = GetMostRestrictiveAvailability(
        availability, GetDevToolsAvailability(primary_user_prefs));
  }
#endif
  return availability;
}

}  // namespace policy
