// 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.

// Defines the Chrome Extensions BrowsingData API functions, which entail
// clearing browsing data, and clearing the browser's cache (which, let's be
// honest, are the same thing), as specified in the extension API JSON.

#include "chrome/browser/extensions/api/browsing_data/browsing_data_api.h"

#include <string>
#include <utility>

#include "base/compiler_specific.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/strings/stringprintf.h"
#include "base/task/thread_pool.h"
#include "chrome/browser/browsing_data/chrome_browsing_data_remover_constants.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/signin/identity_manager_factory.h"
#include "chrome/browser/sync/sync_ui_util.h"
#include "components/browsing_data/content/browsing_data_helper.h"
#include "components/browsing_data/core/features.h"
#include "components/browsing_data/core/pref_names.h"
#include "components/history/core/common/pref_names.h"
#include "components/prefs/pref_service.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/browsing_data_filter_builder.h"
#include "content/public/browser/browsing_data_remover.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/error_utils.h"
#include "extensions/common/extension.h"
#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
#include "third_party/blink/public/mojom/devtools/console_message.mojom.h"

using browsing_data::BrowsingDataType;
using browsing_data::ClearBrowsingDataTab;
using content::BrowserThread;

namespace {

const int64_t kFilterableDataTypes =
    chrome_browsing_data_remover::DATA_TYPE_SITE_DATA |
    content::BrowsingDataRemover::DATA_TYPE_CACHE;

static_assert((kFilterableDataTypes &
               ~chrome_browsing_data_remover::FILTERABLE_DATA_TYPES) == 0,
              "kFilterableDataTypes must be a subset of "
              "chrome_browsing_data_remover::FILTERABLE_DATA_TYPES");

uint64_t MaskForKey(const char* key) {
  if (UNSAFE_TODO(
          strcmp(key, extension_browsing_data_api_constants::kCacheKey)) == 0) {
    return content::BrowsingDataRemover::DATA_TYPE_CACHE;
  }
  if (UNSAFE_TODO(strcmp(
          key, extension_browsing_data_api_constants::kCookiesKey)) == 0) {
    return content::BrowsingDataRemover::DATA_TYPE_COOKIES;
  }
  if (UNSAFE_TODO(strcmp(
          key, extension_browsing_data_api_constants::kDownloadsKey)) == 0) {
    return content::BrowsingDataRemover::DATA_TYPE_DOWNLOADS;
  }
  if (UNSAFE_TODO(strcmp(
          key, extension_browsing_data_api_constants::kFileSystemsKey)) == 0) {
    return content::BrowsingDataRemover::DATA_TYPE_FILE_SYSTEMS;
  }
  if (UNSAFE_TODO(strcmp(
          key, extension_browsing_data_api_constants::kFormDataKey)) == 0) {
    return chrome_browsing_data_remover::DATA_TYPE_FORM_DATA;
  }
  if (UNSAFE_TODO(strcmp(
          key, extension_browsing_data_api_constants::kHistoryKey)) == 0) {
    return chrome_browsing_data_remover::DATA_TYPE_HISTORY;
  }
  if (UNSAFE_TODO(strcmp(
          key, extension_browsing_data_api_constants::kIndexedDBKey)) == 0) {
    return content::BrowsingDataRemover::DATA_TYPE_INDEXED_DB;
  }
  if (UNSAFE_TODO(strcmp(
          key, extension_browsing_data_api_constants::kLocalStorageKey)) == 0) {
    return content::BrowsingDataRemover::DATA_TYPE_LOCAL_STORAGE;
  }
  if (UNSAFE_TODO(strcmp(
          key, extension_browsing_data_api_constants::kServiceWorkersKey)) ==
      0) {
    return content::BrowsingDataRemover::DATA_TYPE_SERVICE_WORKERS;
  }
  if (UNSAFE_TODO(strcmp(
          key, extension_browsing_data_api_constants::kCacheStorageKey)) == 0) {
    return content::BrowsingDataRemover::DATA_TYPE_CACHE_STORAGE;
  }

  return 0ULL;
}

// Returns false if any of the selected data types are not allowed to be
// deleted.
bool IsRemovalPermitted(uint64_t removal_mask, PrefService* prefs) {
  // Enterprise policy or user preference might prohibit deleting browser or
  // download history.
  if ((removal_mask & chrome_browsing_data_remover::DATA_TYPE_HISTORY) ||
      (removal_mask & content::BrowsingDataRemover::DATA_TYPE_DOWNLOADS)) {
    return prefs->GetBoolean(prefs::kAllowDeletingBrowserHistory);
  }
  return true;
}

}  // namespace

bool BrowsingDataSettingsFunction::isDataTypeSelected(
    BrowsingDataType data_type,
    ClearBrowsingDataTab tab) {
  if (data_type == BrowsingDataType::PASSWORDS
#if !BUILDFLAG(IS_ANDROID)
      &&
      base::FeatureList::IsEnabled(browsing_data::features::kDbdRevampDesktop)
#endif  // !BUILDFLAG(IS_ANDROID)
  ) {
    return false;
  }

  std::string pref_name;
  bool success = GetDeletionPreferenceFromDataType(data_type, tab, &pref_name);
  return success && prefs_->GetBoolean(pref_name);
}

ExtensionFunction::ResponseAction BrowsingDataSettingsFunction::Run() {
  prefs_ = Profile::FromBrowserContext(browser_context())->GetPrefs();

  ClearBrowsingDataTab tab = static_cast<ClearBrowsingDataTab>(
      prefs_->GetInteger(browsing_data::prefs::kLastClearBrowsingDataTab));

  // Fill origin types.
  // The "cookies" and "hosted apps" UI checkboxes both map to
  // REMOVE_SITE_DATA in browsing_data_remover.h, the former for the unprotected
  // web, the latter for  protected web data. There is no UI control for
  // extension data.
  base::Value::Dict origin_types;
  origin_types.Set(extension_browsing_data_api_constants::kUnprotectedWebKey,
                   isDataTypeSelected(BrowsingDataType::SITE_DATA, tab));
  origin_types.Set(extension_browsing_data_api_constants::kProtectedWebKey,
                   isDataTypeSelected(BrowsingDataType::HOSTED_APPS_DATA, tab));
  origin_types.Set(extension_browsing_data_api_constants::kExtensionsKey,
                   false);

  // Fill deletion time period.
  int period_pref =
      prefs_->GetInteger(browsing_data::GetTimePeriodPreferenceName(tab));

  browsing_data::TimePeriod period =
      static_cast<browsing_data::TimePeriod>(period_pref);
  double since = 0;
  if (period != browsing_data::TimePeriod::ALL_TIME) {
    base::Time time = browsing_data::CalculateBeginDeleteTime(period);
    since = time.InMillisecondsFSinceUnixEpoch();
  }

  base::Value::Dict options;
  options.Set(extension_browsing_data_api_constants::kOriginTypesKey,
              std::move(origin_types));
  options.Set(extension_browsing_data_api_constants::kSinceKey, since);

  // Fill dataToRemove and dataRemovalPermitted.
  base::Value::Dict selected;
  base::Value::Dict permitted;

  bool delete_site_data =
      isDataTypeSelected(BrowsingDataType::SITE_DATA, tab) ||
      isDataTypeSelected(BrowsingDataType::HOSTED_APPS_DATA, tab);

  SetDetails(&selected, &permitted,
             extension_browsing_data_api_constants::kCookiesKey,
             delete_site_data);
  SetDetails(&selected, &permitted,
             extension_browsing_data_api_constants::kFileSystemsKey,
             delete_site_data);
  SetDetails(&selected, &permitted,
             extension_browsing_data_api_constants::kIndexedDBKey,
             delete_site_data);
  SetDetails(&selected, &permitted,
             extension_browsing_data_api_constants::kLocalStorageKey,
             delete_site_data);
  SetDetails(&selected, &permitted,
             extension_browsing_data_api_constants::kServiceWorkersKey,
             delete_site_data);
  SetDetails(&selected, &permitted,
             extension_browsing_data_api_constants::kCacheStorageKey,
             delete_site_data);
  // PluginData is not supported anymore. (crbug.com/1135791)
  SetDetails(&selected, &permitted,
             extension_browsing_data_api_constants::kPluginDataKeyDeprecated,
             false);
  SetDetails(&selected, &permitted,
             extension_browsing_data_api_constants::kHistoryKey,
             isDataTypeSelected(BrowsingDataType::HISTORY, tab));
  SetDetails(&selected, &permitted,
             extension_browsing_data_api_constants::kDownloadsKey,
             isDataTypeSelected(BrowsingDataType::DOWNLOADS, tab));
  SetDetails(&selected, &permitted,
             extension_browsing_data_api_constants::kCacheKey,
             isDataTypeSelected(BrowsingDataType::CACHE, tab));
  SetDetails(&selected, &permitted,
             extension_browsing_data_api_constants::kFormDataKey,
             isDataTypeSelected(BrowsingDataType::FORM_DATA, tab));
  SetDetails(&selected, &permitted,
             extension_browsing_data_api_constants::kPasswordsKeyDeprecated,
             isDataTypeSelected(BrowsingDataType::PASSWORDS, tab));

  base::Value::Dict result;
  result.Set(extension_browsing_data_api_constants::kOptionsKey,
             std::move(options));
  result.Set(extension_browsing_data_api_constants::kDataToRemoveKey,
             std::move(selected));
  result.Set(extension_browsing_data_api_constants::kDataRemovalPermittedKey,
             std::move(permitted));
  return RespondNow(WithArguments(std::move(result)));
}

void BrowsingDataSettingsFunction::SetDetails(base::Value::Dict* selected_dict,
                                              base::Value::Dict* permitted_dict,
                                              const char* data_type,
                                              bool is_selected) {
  bool is_permitted = IsRemovalPermitted(MaskForKey(data_type), prefs_);
  selected_dict->Set(data_type, is_selected && is_permitted);
  permitted_dict->Set(data_type, is_permitted);
}

BrowsingDataRemoverFunction::BrowsingDataRemoverFunction() = default;

void BrowsingDataRemoverFunction::OnBrowsingDataRemoverDone(
    uint64_t failed_data_types) {
  OnTaskFinished();
}

void BrowsingDataRemoverFunction::OnTaskFinished() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK_GT(pending_tasks_, 0);
  if (--pending_tasks_ > 0)
    return;
  observation_.Reset();
  Respond(NoArguments());
  Release();  // Balanced in StartRemoving.
}

void BrowsingDataRemoverFunction::LogUnsupportedDataTypeWarning(
    const std::string& data_types) {
  WriteToConsole(
      blink::mojom::ConsoleMessageLevel::kWarning,
      base::StringPrintf(
          extension_browsing_data_api_constants::kUnsupportedDataTypeWarning,
          data_types.c_str()));
}

ExtensionFunction::ResponseAction BrowsingDataRemoverFunction::Run() {
  Profile* profile = Profile::FromBrowserContext(browser_context());
  // If we don't have a profile, something's pretty wrong.
  DCHECK(profile);

  // Grab the initial |options| parameter, and parse out the arguments.
  EXTENSION_FUNCTION_VALIDATE(args().size() >= 1);
  EXTENSION_FUNCTION_VALIDATE(args()[0].is_dict());
  const base::Value::Dict& options = args()[0].GetDict();
#if BUILDFLAG(ARKWEB_ARKWEB_EXTENSIONS)
  options_ = options.Clone();
#endif

  EXTENSION_FUNCTION_VALIDATE(ParseOriginTypeMask(options, &origin_type_mask_));

  // If |ms_since_epoch| isn't set, default it to 0.
  double ms_since_epoch =
      options.FindDouble(extension_browsing_data_api_constants::kSinceKey)
          .value_or(0);

  // base::Time takes a double that represents seconds since epoch. JavaScript
  // gives developers milliseconds, so do a quick conversion before populating
  // the object.
  remove_since_ =
      ms_since_epoch == 0
          ? base::Time()
          : base::Time::FromMillisecondsSinceUnixEpoch(ms_since_epoch);

  EXTENSION_FUNCTION_VALIDATE(GetRemovalMask(&removal_mask_));

  if (IsRemovalDeprecated()) {
    return RespondNow(
        Error(extension_browsing_data_api_constants::kDeprecatedDataTypeError));
  }

  const base::Value::List* origins =
      options.FindList(extension_browsing_data_api_constants::kOriginsKey);
  const base::Value::List* exclude_origins = options.FindList(
      extension_browsing_data_api_constants::kExcludeOriginsKey);

  // Check that only |origins| or |excludeOrigins| can be set.
  if (origins && exclude_origins) {
    return RespondNow(
        Error(extension_browsing_data_api_constants::kIncompatibleFilterError));
  }

  if (origins) {
    OriginParsingResult result = ParseOrigins(*origins);
    if (!result.has_value()) {
      return RespondNow(std::move(result.error()));
    }
    EXTENSION_FUNCTION_VALIDATE(!result->empty());

    origins_ = std::move(*result);
  } else if (exclude_origins) {
    OriginParsingResult result = ParseOrigins(*exclude_origins);
    if (!result.has_value()) {
      return RespondNow(std::move(result.error()));
    }
    origins_ = std::move(*result);
  }
  mode_ = origins ? content::BrowsingDataFilterBuilder::Mode::kDelete
                  : content::BrowsingDataFilterBuilder::Mode::kPreserve;

  // Check if a filter is set but non-filterable types are selected.
  if ((!origins_.empty() && (removal_mask_ & ~kFilterableDataTypes) != 0)) {
    return RespondNow(
        Error(extension_browsing_data_api_constants::kNonFilterableError));
  }

  // Check for prohibited data types.
  if (!IsRemovalPermitted(removal_mask_, profile->GetPrefs())) {
    return RespondNow(
        Error(extension_browsing_data_api_constants::kDeleteProhibitedError));
  }
  StartRemoving();

  return did_respond() ? AlreadyResponded() : RespondLater();
}

BrowsingDataRemoverFunction::~BrowsingDataRemoverFunction() = default;

bool BrowsingDataRemoverFunction::IsPauseSyncAllowed() {
  return true;
}

bool BrowsingDataRemoverFunction::IsRemovalDeprecated() {
  return false;
}

#if !BUILDFLAG(ARKWEB_ARKWEB_EXTENSIONS)
void BrowsingDataRemoverFunction::StartRemoving() {
  Profile* profile = Profile::FromBrowserContext(browser_context());
  content::BrowsingDataRemover* remover = profile->GetBrowsingDataRemover();

  // Add a ref (Balanced in OnTaskFinished)
  AddRef();

  // Create a BrowsingDataRemover, set the current object as an observer (so
  // that we're notified after removal) and call remove() with the arguments
  // we've generated above. We can use a raw pointer here, as the browsing data
  // remover is responsible for deleting itself once data removal is complete.
  observation_.Observe(remover);

  DCHECK_EQ(pending_tasks_, 0);
  pending_tasks_ = 1;
  if (removal_mask_ & content::BrowsingDataRemover::DATA_TYPE_COOKIES &&
      !origins_.empty()) {
    pending_tasks_ += 1;
    removal_mask_ &= ~content::BrowsingDataRemover::DATA_TYPE_COOKIES;
    // Cookies are scoped more broadly than origins, so we expand the
    // origin filter to registrable domains in order to match all cookies
    // that could be applied to an origin. This is the same behavior as
    // the Clear-Site-Data header.
    auto filter_builder = content::BrowsingDataFilterBuilder::Create(mode_);
    for (const auto& origin : origins_) {
      std::string domain = GetDomainAndRegistry(
          origin, net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES);
      if (domain.empty())
        domain = origin.host();  // IP address or internal hostname.
      filter_builder->AddRegisterableDomain(domain);
    }
    remover->RemoveWithFilterAndReply(
        remove_since_, base::Time::Max(),
        content::BrowsingDataRemover::DATA_TYPE_COOKIES, origin_type_mask_,
        std::move(filter_builder), this);
  }
  if (removal_mask_) {
    pending_tasks_ += 1;
    auto filter_builder = content::BrowsingDataFilterBuilder::Create(mode_);
    for (const auto& origin : origins_) {
      filter_builder->AddOrigin(origin);
    }
    remover->RemoveWithFilterAndReply(remove_since_, base::Time::Max(),
                                      removal_mask_, origin_type_mask_,
                                      std::move(filter_builder), this);
  }
  OnTaskFinished();
}
#endif

bool BrowsingDataRemoverFunction::ParseOriginTypeMask(
    const base::Value::Dict& options,
    uint64_t* origin_type_mask) {
  // Parse the |options| dictionary to generate the origin set mask. Default to
  // UNPROTECTED_WEB if the developer doesn't specify anything.
  *origin_type_mask = content::BrowsingDataRemover::ORIGIN_TYPE_UNPROTECTED_WEB;

  const base::Value* origin_type_dict =
      options.Find(extension_browsing_data_api_constants::kOriginTypesKey);
  if (!origin_type_dict)
    return true;

  if (!origin_type_dict->is_dict())
    return false;

  const base::Value::Dict& origin_type = origin_type_dict->GetDict();

  // The developer specified something! Reset to 0 and parse the dictionary.
  *origin_type_mask = 0;

  // Unprotected web.
  const base::Value* option = origin_type.Find(
      extension_browsing_data_api_constants::kUnprotectedWebKey);
  if (option) {
    if (!option->is_bool())
      return false;

    *origin_type_mask |=
        option->GetBool()
            ? content::BrowsingDataRemover::ORIGIN_TYPE_UNPROTECTED_WEB
            : 0;
  }

  // Protected web.
  option =
      origin_type.Find(extension_browsing_data_api_constants::kProtectedWebKey);
  if (option) {
    if (!option->is_bool())
      return false;

    *origin_type_mask |=
        option->GetBool()
            ? content::BrowsingDataRemover::ORIGIN_TYPE_PROTECTED_WEB
            : 0;
  }

  // Extensions.
  option =
      origin_type.Find(extension_browsing_data_api_constants::kExtensionsKey);
  if (option) {
    if (!option->is_bool())
      return false;

    *origin_type_mask |=
        option->GetBool() ? chrome_browsing_data_remover::ORIGIN_TYPE_EXTENSION
                          : 0;
  }

  return true;
}

BrowsingDataRemoverFunction::OriginParsingResult
BrowsingDataRemoverFunction::ParseOrigins(const base::Value::List& list_value) {
  std::vector<url::Origin> result;
  result.reserve(list_value.size());
  for (const auto& value : list_value) {
    if (!value.is_string()) {
      return base::unexpected(BadMessage());
    }
    url::Origin origin = url::Origin::Create(GURL(value.GetString()));
    if (origin.opaque()) {
      return base::unexpected(Error(base::StringPrintf(
          extension_browsing_data_api_constants::kInvalidOriginError,
          value.GetString().c_str())));
    }
    result.push_back(std::move(origin));
  }
  return result;
}

// Parses the |dataToRemove| argument to generate the removal mask.
// Returns false if parse was not successful, i.e. if 'dataToRemove' is not
// present or any data-type keys don't have supported (boolean) values.
bool BrowsingDataRemoveFunction::GetRemovalMask(uint64_t* removal_mask) {
  if (args().size() <= 1 || !args()[1].is_dict())
    return false;

  std::vector<std::string> unsupported_data_types;
  *removal_mask = 0;
  for (const auto kv : args()[1].GetDict()) {
    if (!kv.second.is_bool())
      return false;
    if (kv.second.GetBool()) {
      uint64_t mask = MaskForKey(kv.first.c_str());
      if (mask == 0) {
        unsupported_data_types.push_back(kv.first);
      } else {
        *removal_mask |= mask;
      }
    }
  }

  if (!unsupported_data_types.empty()) {
    LogUnsupportedDataTypeWarning(
        base::JoinString(unsupported_data_types, ", "));
  }

  return true;
}

bool BrowsingDataRemoveFunction::IsPauseSyncAllowed() {
  return false;
}

bool BrowsingDataRemoveAppcacheFunction::GetRemovalMask(
    uint64_t* removal_mask) {
  // TODO(http://crbug.com/1266606): deprecate and remove this extension api
  *removal_mask = 0;
  LogUnsupportedDataTypeWarning("appcache");
  return true;
}

bool BrowsingDataRemoveCacheFunction::GetRemovalMask(uint64_t* removal_mask) {
  *removal_mask = content::BrowsingDataRemover::DATA_TYPE_CACHE;
  return true;
}

bool BrowsingDataRemoveCookiesFunction::GetRemovalMask(uint64_t* removal_mask) {
  *removal_mask = content::BrowsingDataRemover::DATA_TYPE_COOKIES;
  return true;
}

bool BrowsingDataRemoveDownloadsFunction::GetRemovalMask(
    uint64_t* removal_mask) {
  *removal_mask = content::BrowsingDataRemover::DATA_TYPE_DOWNLOADS;
  return true;
}

bool BrowsingDataRemoveFileSystemsFunction::GetRemovalMask(
    uint64_t* removal_mask) {
  *removal_mask = content::BrowsingDataRemover::DATA_TYPE_FILE_SYSTEMS;
  return true;
}

bool BrowsingDataRemoveFormDataFunction::GetRemovalMask(
    uint64_t* removal_mask) {
  *removal_mask = chrome_browsing_data_remover::DATA_TYPE_FORM_DATA;
  return true;
}

bool BrowsingDataRemoveHistoryFunction::GetRemovalMask(uint64_t* removal_mask) {
  *removal_mask = chrome_browsing_data_remover::DATA_TYPE_HISTORY;
  return true;
}

bool BrowsingDataRemoveIndexedDBFunction::GetRemovalMask(
    uint64_t* removal_mask) {
  *removal_mask = content::BrowsingDataRemover::DATA_TYPE_INDEXED_DB;
  return true;
}

bool BrowsingDataRemoveLocalStorageFunction::GetRemovalMask(
    uint64_t* removal_mask) {
  *removal_mask = content::BrowsingDataRemover::DATA_TYPE_LOCAL_STORAGE;
  return true;
}

bool BrowsingDataRemovePluginDataFunction::GetRemovalMask(
    uint64_t* removal_mask) {
  // Plugin data is not supported anymore. (crbug.com/1135788)
  *removal_mask = 0;
  LogUnsupportedDataTypeWarning(
      extension_browsing_data_api_constants::kPluginDataKeyDeprecated);
  return true;
}

bool BrowsingDataRemovePasswordsFunction::GetRemovalMask(
    uint64_t* removal_mask) {
  // Password deletion is not supported anymore.
  *removal_mask = 0;
  LogUnsupportedDataTypeWarning(
      extension_browsing_data_api_constants::kPasswordsKeyDeprecated);
  return true;
}

bool BrowsingDataRemovePasswordsFunction::IsRemovalDeprecated() {
#if !BUILDFLAG(IS_ANDROID)
  if (base::FeatureList::IsEnabled(
          browsing_data::features::kPasswordRemovalExtensionErrorKillSwitch)) {
    return true;
  }
#endif  // !BUILDFLAG(IS_ANDROID)
  return false;
}

bool BrowsingDataRemoveServiceWorkersFunction::GetRemovalMask(
    uint64_t* removal_mask) {
  *removal_mask = content::BrowsingDataRemover::DATA_TYPE_SERVICE_WORKERS;
  return true;
}

bool BrowsingDataRemoveCacheStorageFunction::GetRemovalMask(
    uint64_t* removal_mask) {
  *removal_mask = content::BrowsingDataRemover::DATA_TYPE_CACHE_STORAGE;
  return true;
}

bool BrowsingDataRemoveWebSQLFunction::GetRemovalMask(uint64_t* removal_mask) {
  // TODO(http://crbug.com/420857719): Deprecate and remove this extension api.
  *removal_mask = 0;
  LogUnsupportedDataTypeWarning("webSQL");
  return true;
}