// Copyright 2013 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/policy_handlers.h"

#include <stddef.h>

#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <unordered_set>
#include <utility>
#include <vector>

#include "base/check.h"
#include "base/notreached.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/extensions/extension_management_constants.h"
#include "chrome/browser/extensions/external_policy_loader.h"
#include "components/crx_file/id_util.h"
#include "components/policy/core/browser/configuration_policy_handler.h"
#include "components/policy/core/browser/policy_error_map.h"
#include "components/policy/core/common/policy_map.h"
#include "components/policy/core/common/schema.h"
#include "components/policy/policy_constants.h"
#include "components/prefs/pref_value_map.h"
#include "components/strings/grit/components_strings.h"
#include "extensions/browser/pref_names.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/extension_urls.h"
#include "extensions/common/url_pattern.h"
#include "url/gurl.h"

#if BUILDFLAG(IS_WIN)
#include "base/enterprise_util.h"
#endif

static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));

namespace extensions {
namespace {
// Returns true if extensions_ids contains a list of valid extension ids,
// divided by comma.
bool IsValidIdList(const std::string& extension_ids) {
  std::vector<std::string_view> ids = base::SplitStringPiece(
      extension_ids, ",", base::WhitespaceHandling::TRIM_WHITESPACE,
      base::SplitResult::SPLIT_WANT_NONEMPTY);
  if (ids.size() == 0) {
    return false;
  }
  for (const auto& id : ids) {
    if (!crx_file::id_util::IdIsValid(id)) {
      return false;
    }
  }
  return true;
}

// Returns true if URL is valid and uses one of the supported schemes.
bool IsValidUpdateUrl(const std::string& update_url) {
  GURL update_gurl(update_url);
  if (!update_gurl.is_valid()) {
    return false;
  }
  return update_gurl.SchemeIsHTTPOrHTTPS() || update_gurl.SchemeIsFile();
}

}  // namespace

// ExtensionListPolicyHandler implementation -----------------------------------

ExtensionListPolicyHandler::ExtensionListPolicyHandler(const char* policy_name,
                                                       const char* pref_path,
                                                       bool allow_wildcards)
    : policy::ListPolicyHandler(policy_name, base::Value::Type::STRING),
      pref_path_(pref_path),
      allow_wildcards_(allow_wildcards) {}

ExtensionListPolicyHandler::~ExtensionListPolicyHandler() = default;

bool ExtensionListPolicyHandler::CheckListEntry(const base::Value& value) {
  const std::string& str = value.GetString();
  if (allow_wildcards_ && str == "*") {
    return true;
  }

  // Make sure str is an extension id.
  return crx_file::id_util::IdIsValid(str);
}

void ExtensionListPolicyHandler::ApplyList(base::ListValue filtered_list,
                                           PrefValueMap* prefs) {
  prefs->SetValue(pref_path_, base::Value(std::move(filtered_list)));
}

// ExtensionInstallForceListPolicyHandler implementation -----------------------

ExtensionInstallForceListPolicyHandler::ExtensionInstallForceListPolicyHandler()
    : policy::TypeCheckingPolicyHandler(policy::key::kExtensionInstallForcelist,
                                        base::Value::Type::LIST) {}

bool ExtensionInstallForceListPolicyHandler::CheckPolicySettings(
    const policy::PolicyMap& policies,
    policy::PolicyErrorMap* errors) {
  const base::Value* value;
  return CheckAndGetValue(policies, errors, &value) &&
         ParseList(value, nullptr, errors);
}

void ExtensionInstallForceListPolicyHandler::ApplyPolicySettings(
    const policy::PolicyMap& policies,
    PrefValueMap* prefs) {
  auto dict = GetPolicyDict(policies);

  if (dict.has_value()) {
    prefs->SetValue(pref_names::kInstallForceList,
                    base::Value(std::move(dict).value()));
  }
}

bool ExtensionInstallForceListPolicyHandler::ParseList(
    const base::Value* policy_value,
    base::DictValue* extension_dict,
    policy::PolicyErrorMap* errors) {
  if (!policy_value) {
    return true;
  }

  if (!policy_value->is_list()) {
    // This should have been caught in CheckPolicySettings.
    NOTREACHED();
  }

  int index = -1;
  for (const auto& entry : policy_value->GetList()) {
    ++index;
    if (!entry.is_string()) {
      if (errors) {
        errors->AddError(policy_name(), IDS_POLICY_TYPE_ERROR,
                         base::Value::GetTypeName(base::Value::Type::STRING),
                         policy::PolicyErrorPath{index});
      }
      continue;
    }
    std::string entry_string = entry.GetString();

    // Each string item of the list should be of one of the following forms:
    // * <extension_id>
    // * <extension_id>;<update_url>
    // Note: The update URL might also contain semicolons.
    std::string extension_id;
    std::string update_url;
    size_t pos = entry_string.find(';');
    if (pos == std::string::npos) {
      extension_id = entry_string;
      update_url = extension_urls::kChromeWebstoreUpdateURL;
    } else {
      extension_id = entry_string.substr(0, pos);
      update_url = entry_string.substr(pos + 1);
    }

    if (!crx_file::id_util::IdIsValid(extension_id)) {
      if (errors) {
        errors->AddError(policy_name(), IDS_POLICY_INVALID_EXTENSION_ID_ERROR,
                         policy::PolicyErrorPath{index});
      }
      continue;
    }

    // Check that url is valid and uses one of the supported schemes.
    if (!IsValidUpdateUrl(update_url)) {
      if (errors) {
        errors->AddError(policy_name(), IDS_POLICY_INVALID_UPDATE_URL_ERROR,
                         extension_id, policy::PolicyErrorPath{index});
      }
      continue;
    }

    if (extension_dict) {
      ExternalPolicyLoader::AddExtension(*extension_dict, extension_id,
                                         update_url);
    }
  }

  return true;
}

std::optional<base::DictValue>
ExtensionInstallForceListPolicyHandler::GetPolicyDict(
    const policy::PolicyMap& policies) {
  const base::Value* value = nullptr;
  base::DictValue dict;
  if (CheckAndGetValue(policies, nullptr, &value) && value &&
      ParseList(value, &dict, nullptr)) {
    return dict;
  }
  return std::nullopt;
}

// ExtensionInstallBlockListPolicyHandler implementation ----------------------

ExtensionInstallBlockListPolicyHandler::ExtensionInstallBlockListPolicyHandler()
    : list_handler_(policy::key::kExtensionInstallBlocklist,
                    pref_names::kInstallDenyList,
                    /*allow_wildcards*/ true) {}

ExtensionInstallBlockListPolicyHandler::
    ~ExtensionInstallBlockListPolicyHandler() = default;

bool ExtensionInstallBlockListPolicyHandler::CheckPolicySettings(
    const policy::PolicyMap& policies,
    policy::PolicyErrorMap* errors) {
  return list_handler_.CheckPolicySettings(policies, errors);
}

void ExtensionInstallBlockListPolicyHandler::ApplyPolicySettings(
    const policy::PolicyMap& policies,
    PrefValueMap* prefs) {
  list_handler_.ApplyPolicySettings(policies, prefs);
}

// ExtensionURLPatternListPolicyHandler implementation -------------------------

ExtensionURLPatternListPolicyHandler::ExtensionURLPatternListPolicyHandler(
    const char* policy_name,
    const char* pref_path)
    : policy::TypeCheckingPolicyHandler(policy_name, base::Value::Type::LIST),
      pref_path_(pref_path) {}

ExtensionURLPatternListPolicyHandler::~ExtensionURLPatternListPolicyHandler() =
    default;

bool ExtensionURLPatternListPolicyHandler::CheckPolicySettings(
    const policy::PolicyMap& policies,
    policy::PolicyErrorMap* errors) {
  const base::Value* value = nullptr;
  if (!CheckAndGetValue(policies, errors, &value)) {
    return false;
  }

  if (!value) {
    return true;
  }

  if (!value->is_list()) {
    NOTREACHED();
  }

  // Check that the list contains valid URLPattern strings only.
  int index = 0;
  for (const auto& entry : value->GetList()) {
    if (!entry.is_string()) {
      errors->AddError(policy_name(), IDS_POLICY_TYPE_ERROR,
                       base::Value::GetTypeName(base::Value::Type::STRING),
                       policy::PolicyErrorPath{index});
      return false;
    }
    std::string url_pattern_string = entry.GetString();

    URLPattern pattern(URLPattern::SCHEME_ALL);
    if (pattern.Parse(url_pattern_string) !=
        URLPattern::ParseResult::kSuccess) {
      errors->AddError(policy_name(), IDS_POLICY_INVALID_URL_ERROR,
                       policy::PolicyErrorPath{index});
      return false;
    }
    ++index;
  }

  return true;
}

void ExtensionURLPatternListPolicyHandler::ApplyPolicySettings(
    const policy::PolicyMap& policies,
    PrefValueMap* prefs) {
  if (!pref_path_) {
    return;
  }
  // It is safe to use `GetValueUnsafe()` as multiple policy types are handled.
  const base::Value* value = policies.GetValueUnsafe(policy_name());
  if (value) {
    prefs->SetValue(pref_path_, value->Clone());
  }
}

// ExtensionSettingsPolicyHandler implementation  ------------------------------

ExtensionSettingsPolicyHandler::ExtensionSettingsPolicyHandler(
    const policy::Schema& chrome_schema)
    : policy::SchemaValidatingPolicyHandler(
          policy::key::kExtensionSettings,
          chrome_schema.GetKnownProperty(policy::key::kExtensionSettings),
          policy::SCHEMA_ALLOW_UNKNOWN) {}

ExtensionSettingsPolicyHandler::~ExtensionSettingsPolicyHandler() = default;

void ExtensionSettingsPolicyHandler::SanitizePolicySettings(
    base::Value* policy_value,
    policy::PolicyErrorMap* errors) {
  DCHECK(policy_value);

  // |policy_value| is expected to conform to the defined schema. But it's
  // not strictly valid since there are additional restrictions.
  DCHECK(policy_value->is_dict());

  // Dictionary entries with any invalid setting get removed at the end. We
  // can't mutate the dict while iterating, so store them here.
  std::unordered_set<std::string> invalid_keys;

  // Check each entry, populating |invalid_keys| and |errors|.
  for (const auto [extension_ids, policy] : policy_value->GetDict()) {
    DCHECK(extension_ids == schema_constants::kWildcard ||
           IsValidIdList(extension_ids));
    DCHECK(policy.is_dict());

    // Extracts sub dictionary.
    const base::DictValue& sub_dict = policy.GetDict();

    const std::string* installation_mode =
        sub_dict.FindString(schema_constants::kInstallationMode);
    if (installation_mode) {
      if (*installation_mode == schema_constants::kForceInstalled ||
          *installation_mode == schema_constants::kNormalInstalled) {
        DCHECK(extension_ids != schema_constants::kWildcard);
        // Verifies that 'update_url' is specified for 'force_installed' and
        // 'normal_installed' mode.
        const std::string* update_url =
            sub_dict.FindString(schema_constants::kUpdateUrl);
        if (!update_url || update_url->empty()) {
          if (errors) {
            errors->AddError(policy_name(), IDS_POLICY_NOT_SPECIFIED_ERROR,
                             policy::PolicyErrorPath{
                                 extension_ids, schema_constants::kUpdateUrl});
          }
          invalid_keys.insert(extension_ids);
          continue;
        }

        // Check that url is valid and uses one of the supported schemes.
        if (!IsValidUpdateUrl(*update_url)) {
          if (errors) {
            errors->AddError(policy_name(), IDS_POLICY_INVALID_UPDATE_URL_ERROR,
                             extension_ids);
          }
          invalid_keys.insert(extension_ids);
          continue;
        }
      }
    }
    // Host keys that don't support user defined paths.
    const char* host_keys[] = {schema_constants::kPolicyBlockedHosts,
                               schema_constants::kPolicyAllowedHosts};
    const int extension_scheme_mask =
        URLPattern::GetValidSchemeMaskForExtensions();
    for (const char* key : host_keys) {
      const base::ListValue* unparsed_urls = sub_dict.FindList(key);
      if (unparsed_urls != nullptr) {
        for (const auto& url_value : *unparsed_urls) {
          const std::string& unparsed_url = url_value.GetString();
          URLPattern pattern(extension_scheme_mask);
          URLPattern::ParseResult parse_result = pattern.Parse(unparsed_url);
          // These keys don't support paths due to how we track the initiator
          // of a webRequest and cookie security policy. We expect a valid
          // pattern to return a PARSE_ERROR_EMPTY_PATH.
          if (parse_result == URLPattern::ParseResult::kEmptyPath) {
            // Add a wildcard path to the URL as it should match any path.
            parse_result = pattern.Parse(unparsed_url + "/*");
          } else if (parse_result == URLPattern::ParseResult::kSuccess) {
            // The user supplied a path, notify them that this is not supported.
            if (!pattern.match_all_urls()) {
              if (errors) {
                errors->AddError(
                    policy_name(), IDS_POLICY_URL_PATH_SPECIFIED_ERROR,
                    unparsed_url, policy::PolicyErrorPath{extension_ids, key});
              }
              invalid_keys.insert(extension_ids);
              break;
            }
          }
          if (parse_result != URLPattern::ParseResult::kSuccess) {
            if (errors) {
              errors->AddError(policy_name(), IDS_POLICY_INVALID_URL_ERROR,
                               policy::PolicyErrorPath{extension_ids, key});
            }
            invalid_keys.insert(extension_ids);
            break;
          }
        }
      }
    }

    const base::ListValue* runtime_blocked_hosts =
        sub_dict.FindList(schema_constants::kPolicyBlockedHosts);
    if (runtime_blocked_hosts != nullptr &&
        runtime_blocked_hosts->size() >
            schema_constants::kMaxItemsURLPatternSet) {
      if (errors) {
        policy::PolicyErrorPath error_path = {
            extension_ids, schema_constants::kPolicyBlockedHosts};
        errors->AddError(
            policy_name(), IDS_POLICY_EXTENSION_SETTINGS_ORIGIN_LIMIT_WARNING,
            base::NumberToString(schema_constants::kMaxItemsURLPatternSet),
            error_path);
      }
    }

    const base::ListValue* runtime_allowed_hosts =
        sub_dict.FindList(schema_constants::kPolicyAllowedHosts);
    if (runtime_allowed_hosts != nullptr &&
        runtime_allowed_hosts->size() >
            schema_constants::kMaxItemsURLPatternSet) {
      if (errors) {
        policy::PolicyErrorPath error_path = {
            extension_ids, schema_constants::kPolicyAllowedHosts};
        errors->AddError(
            policy_name(), IDS_POLICY_EXTENSION_SETTINGS_ORIGIN_LIMIT_WARNING,
            base::NumberToString(schema_constants::kMaxItemsURLPatternSet),
            error_path);
      }
    }
  }

  // Remove |invalid_keys| from the dictionary.
  for (const std::string& key : invalid_keys) {
    policy_value->GetDict().Remove(key);
  }
}

bool ExtensionSettingsPolicyHandler::CheckPolicySettings(
    const policy::PolicyMap& policies,
    policy::PolicyErrorMap* errors) {
  checked_value_.reset();
  check_called_ = true;
  std::unique_ptr<base::Value> policy_value;
  if (!CheckAndGetValue(policies, errors, &policy_value)) {
    return false;
  }
  if (!policy_value) {
    return true;
  }

  SanitizePolicySettings(policy_value.get(), errors);
  checked_value_ = std::move(policy_value);
  return true;
}

void ExtensionSettingsPolicyHandler::ApplyPolicySettings(
    const policy::PolicyMap& policies,
    PrefValueMap* prefs) {
  // CheckPolicySettings() must be called before ApplyPolicySettings().
  // This is guaranteed by the framework (configuration_policy_handler_list.cc)
  // and documented in ConfigurationPolicyHandler::ApplyPolicySettingsWithParameters().
  CHECK(check_called_);
  check_called_ = false;

  if (!checked_value_) {
    return;
  }

  prefs->SetValue(pref_names::kExtensionManagement,
                  base::Value::FromUniquePtrValue(std::move(checked_value_)));
}

}  // namespace extensions
