// Copyright 2020 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/enterprise/remote_commands/clear_browsing_data_job.h"

#include <optional>

#include "base/functional/bind.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/task/single_thread_task_runner.h"
#include "base/values.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "components/policy/core/common/policy_logger.h"
#include "content/public/browser/browsing_data_remover.h"

namespace enterprise_commands {

namespace {

const char kFailedTypesPath[] = "failed_data_types";

const char kClearCacheField[] = "clear_cache";
const char kClearCookiesField[] = "clear_cookies";

// Define the possibly failed data types here for 2 reasons:
//
// 1. This will be easier to keep in sync with the server, as the latter
// doesn't care about *all* the types in BrowsingDataRemover.
//
// 2. Centralize handling the underlying type of the values here.
// BrowsingDataRemover represents failed types as uint64_t, which isn't
// natively supported by base::Value, so this class needs to convert to a
// type that's supported. This will also allow us to use a list instead of a
// bit mask, which will be easier to parse gracefully on the server in case
// more types are added.
enum class DataTypes {
  kCache = 0,
  kCookies = 1,
};

std::string CreatePayload(uint64_t failed_data_types) {
  base::DictValue root;
  base::ListValue failed_types_list;

  if (failed_data_types & content::BrowsingDataRemover::DATA_TYPE_CACHE)
    failed_types_list.Append(static_cast<int>(DataTypes::kCache));

  if (failed_data_types & content::BrowsingDataRemover::DATA_TYPE_COOKIES)
    failed_types_list.Append(static_cast<int>(DataTypes::kCookies));

  root.Set(kFailedTypesPath, std::move(failed_types_list));

  return base::WriteJson(root).value_or("");
}

}  // namespace

ClearBrowsingDataJob::ClearBrowsingDataJob(ProfileManager* profile_manager)
    : job_profile_picker_(profile_manager) {}
ClearBrowsingDataJob::ClearBrowsingDataJob(Profile* profile)
    : job_profile_picker_(profile) {}

ClearBrowsingDataJob::~ClearBrowsingDataJob() = default;

enterprise_management::RemoteCommand_Type ClearBrowsingDataJob::GetType()
    const {
  return enterprise_management::RemoteCommand_Type_BROWSER_CLEAR_BROWSING_DATA;
}

bool ClearBrowsingDataJob::ParseCommandPayload(
    const std::string& command_payload) {
  VLOG_POLICY(2, REMOTE_COMMANDS)
      << "Clear browsing data command payload: " << command_payload;
  std::optional<base::DictValue> root = base::JSONReader::ReadDict(
      command_payload, base::JSON_PARSE_CHROMIUM_EXTENSIONS);
  if (!root)
    return false;

  if (!job_profile_picker_.ParseCommandPayload(*root)) {
    return false;
  }

  // Not specifying these fields is equivalent to setting them to false.
  clear_cache_ = root->FindBool(kClearCacheField).value_or(false);
  clear_cookies_ = root->FindBool(kClearCookiesField).value_or(false);

  return true;
}

void ClearBrowsingDataJob::RunImpl(CallbackWithResult result_callback) {
  uint64_t types = 0;
  if (clear_cache_)
    types |= content::BrowsingDataRemover::DATA_TYPE_CACHE;

  if (clear_cookies_)
    types |= content::BrowsingDataRemover::DATA_TYPE_COOKIES;

  Profile* profile = job_profile_picker_.GetProfile();
  if (!profile) {
    // If the payload's profile path doesn't correspond to an existing profile,
    // there's nothing to do. The most likely scenario is that the profile was
    // deleted by the time the command was received.
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE,
        base::BindOnce(std::move(result_callback), policy::ResultType::kFailure,
                       CreatePayload(types)));
    return;
  }

  result_callback_ = std::move(result_callback);

  if (types == 0) {
    LOG_POLICY(WARNING, REMOTE_COMMANDS)
        << "Clear browsing data command has not specified any "
           "data types. Please double check the payload to "
           "make sure everything is set as required.";
    // There's nothing to clear, invoke the callback with success result and be
    // done.
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(std::move(result_callback_),
                                  policy::ResultType::kSuccess,
                                  CreatePayload(
                                      /*failed_data_types=*/0)));
    return;
  }

  content::BrowsingDataRemover* remover = profile->GetBrowsingDataRemover();
  remover->AddObserver(this);

  remover->RemoveAndReply(
      base::Time(), base::Time::Max(), types,
      content::BrowsingDataRemover::ORIGIN_TYPE_UNPROTECTED_WEB, this);
}

void ClearBrowsingDataJob::OnBrowsingDataRemoverDone(
    uint64_t failed_data_types) {
  Profile* profile = job_profile_picker_.GetProfile();
  DCHECK(profile);

  content::BrowsingDataRemover* remover = profile->GetBrowsingDataRemover();
  remover->RemoveObserver(this);

  std::string payload = CreatePayload(failed_data_types);

  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE,
      base::BindOnce(std::move(result_callback_),
                     failed_data_types != 0 ? policy::ResultType::kFailure
                                            : policy::ResultType::kSuccess,
                     std::move(payload)));
}

}  // namespace enterprise_commands
