// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/permissions/permission_decision_auto_blocker.h"

#include <algorithm>
#include <memory>
#include <string>
#include <utility>

#include "base/feature_list.h"
#include "base/metrics/field_trial_params.h"
#include "base/strings/string_number_conversions.h"
#include "base/values.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "components/keyed_service/content/browser_context_dependency_manager.h"
#include "components/permissions/features.h"
#if BUILDFLAG(IS_ANDROID)
#include "components/permissions/android/permissions_android_feature_map.h"
#endif
#include "components/permissions/permission_util.h"
#include "url/gurl.h"

namespace permissions {
namespace {

using PermissionStatus = blink::mojom::PermissionStatus;

// The number of times that users may explicitly dismiss a permission prompt
// from an origin before it is automatically blocked.
constexpr int kDefaultDismissalsBeforeBlock = 3;

// The number of times that users may ignore a permission prompt from an origin
// before it is automatically blocked.
constexpr int kDefaultIgnoresBeforeBlock = 4;

#if BUILDFLAG(IS_ANDROID)
// The number of times that users may ignore a permission prompt from an origin.
// before it is automatically blocked. This is used for the Clapper UI.
constexpr int kClapperIgnoresBeforeBlock = 2;
#endif

// The number of times that users may dismiss a permission prompt that uses the
// quiet UI from an origin before it is automatically blocked.
constexpr int kDefaultDismissalsBeforeBlockWithQuietUi = 1;

// The number of times that users may ignore a permission prompt that uses the
// quiet UI from an origin before it is automatically blocked.
constexpr int kDefaultIgnoresBeforeBlockWithQuietUi = 2;

// The number of days that an origin will stay under embargo for a requested
// permission due to repeated ignores or dismissals.
constexpr int kDefaultEmbargoDays = 7;

// The number of times that users may explicitly dismiss a
// FEDERATED_IDENTITY_API permission prompt from an origin before it is
// automatically blocked.
constexpr int kFederatedIdentityApiDismissalsBeforeBlock = 1;

// The durations that an origin will stay under embargo for the
// FEDERATED_IDENTITY_API permission due to the user explicitly dismissing the
// permission prompt.
constexpr auto kFederatedIdentityApiEmbargoDurationDismiss =
    std::to_array<base::TimeDelta>({base::Hours(2) /* 1st dismissal */,
                                    base::Days(1) /* 2nd dismissal */,
                                    base::Days(7), base::Days(28)});

// The duration that an origin will stay under embargo for the
// FEDERATED_IDENTITY_AUTO_REAUTHN_PERMISSION permission due to an auto re-authn
// prompt being displayed recently.
constexpr base::TimeDelta kFederatedIdentityAutoReauthnEmbargoDuration =
    base::Minutes(10);

// The duration that an origin will stay under embargo for the
// SUB_APP_INSTALLATION_PROMPTS permission when the embargo is applied
// for the first time. After another dismissal, the default kDefaultEmbargoDays
// is applied.
constexpr base::TimeDelta kSubAppInstallationPromptsFirstTimeEmbargoDuration =
    base::Minutes(10);

std::string GetStringForContentType(ContentSettingsType content_type) {
  switch (content_type) {
    case ContentSettingsType::AUTO_PICTURE_IN_PICTURE:
      return "AutoPictureInPicture";
    case ContentSettingsType::FEDERATED_IDENTITY_API:
      return "FederatedIdentityApi";
    case ContentSettingsType::FEDERATED_IDENTITY_AUTO_REAUTHN_PERMISSION:
      return "FederatedIdentityAutoReauthn";
    case ContentSettingsType::FILE_SYSTEM_ACCESS_RESTORE_PERMISSION:
      return "FileSystemAccessRestorePermission";
    case ContentSettingsType::FILE_SYSTEM_WRITE_GUARD:
      return "FileSystemWriteGuard";
    case ContentSettingsType::SUB_APP_INSTALLATION_PROMPTS:
      return "SubAppInstallationPrompts";
#if BUILDFLAG(IS_CHROMEOS)
    case ContentSettingsType::SMART_CARD_GUARD:
      return "SmartCard";
#endif  // BUILDFLAG(IS_CHROMEOS)
    // If you add a new Content Setting here, also add it to
    // IsEnabledForContentSetting.
    default:
      return PermissionUtil::GetPermissionString(content_type);
  }
}

base::DictValue GetOriginAutoBlockerData(HostContentSettingsMap* settings,
                                         const GURL& origin_url) {
  base::Value website_setting = settings->GetWebsiteSetting(
      origin_url, GURL(), ContentSettingsType::PERMISSION_AUTOBLOCKER_DATA);
  if (!website_setting.is_dict()) {
    return base::DictValue();
  }

  return std::move(website_setting.GetDict());
}

base::DictValue* GetOrCreatePermissionDict(base::DictValue& origin_dict,
                                           const std::string& permission) {
  return origin_dict.EnsureDict(permission);
}

int RecordActionInWebsiteSettings(const GURL& url,
                                  ContentSettingsType permission,
                                  const char* key,
                                  HostContentSettingsMap* settings_map) {
  base::DictValue dict = GetOriginAutoBlockerData(settings_map, url);

  base::DictValue* permission_dict =
      GetOrCreatePermissionDict(dict, GetStringForContentType(permission));

  std::optional<int> value = permission_dict->FindInt(key);
  int current_count = value.value_or(0);
  permission_dict->Set(key, base::Value(++current_count));

  settings_map->SetWebsiteSettingDefaultScope(
      url, GURL(), ContentSettingsType::PERMISSION_AUTOBLOCKER_DATA,
      base::Value(std::move(dict)));

  return current_count;
}

int GetActionCount(const GURL& url,
                   ContentSettingsType permission,
                   const char* key,
                   HostContentSettingsMap* settings_map) {
  base::DictValue dict = GetOriginAutoBlockerData(settings_map, url);
  base::DictValue* permission_dict =
      GetOrCreatePermissionDict(dict, GetStringForContentType(permission));

  std::optional<int> value = permission_dict->FindInt(key);
  return value.value_or(0);
}

// Returns the number of times that users may explicitly dismiss a permission
// prompt for an origin for the passed-in |permission| before it is
// automatically blocked.
int GetDismissalsBeforeBlockForContentSettingsType(
    ContentSettingsType permission) {
  return (permission == ContentSettingsType::FEDERATED_IDENTITY_API)
             ? kFederatedIdentityApiDismissalsBeforeBlock
             : kDefaultDismissalsBeforeBlock;
}

// The duration that an origin will stay under embargo for the passed-in
// |permission| due to the user explicitly dismissing the permission prompt.
base::TimeDelta GetEmbargoDurationForContentSettingsType(
    ContentSettingsType permission,
    int dismiss_count) {
  if (permission == ContentSettingsType::FEDERATED_IDENTITY_API) {
    int duration_index =
        std::clamp(dismiss_count - 1, 0,
                   static_cast<int>(
                       kFederatedIdentityApiEmbargoDurationDismiss.size() - 1));
    return kFederatedIdentityApiEmbargoDurationDismiss[duration_index];
  }

  if (permission ==
      ContentSettingsType::FEDERATED_IDENTITY_AUTO_REAUTHN_PERMISSION) {
    return kFederatedIdentityAutoReauthnEmbargoDuration;
  }

  if (permission == ContentSettingsType::SUB_APP_INSTALLATION_PROMPTS) {
    // If this is the first time this embargo is applied, be more forgiving.
    if (dismiss_count == kDefaultDismissalsBeforeBlock) {
      return kSubAppInstallationPromptsFirstTimeEmbargoDuration;
    }
  }

  return base::Days(kDefaultEmbargoDays);
}

base::Time GetEmbargoStartTime(base::DictValue* permission_dict,
                               const char* key) {
  std::optional<double> found = permission_dict->FindDouble(key);
  if (found) {
    return base::Time::FromDeltaSinceWindowsEpoch(base::Microseconds(*found));
  }
  return base::Time();
}

bool IsUnderEmbargo(base::DictValue* permission_dict,
                    const char* key,
                    base::Time current_time,
                    base::TimeDelta offset) {
  std::optional<double> found = permission_dict->FindDouble(key);
  if (found && current_time < base::Time::FromInternalValue(*found) + offset) {
    return true;
  }

  return false;
}
}  // namespace

// static
const char PermissionDecisionAutoBlocker::kPromptDismissCountKey[] =
    "dismiss_count";

// static
const char PermissionDecisionAutoBlocker::kPromptIgnoreCountKey[] =
    "ignore_count";

// static
const char PermissionDecisionAutoBlocker::kPromptDismissCountWithQuietUiKey[] =
    "dismiss_count_quiet_ui";

// static
const char PermissionDecisionAutoBlocker::kPromptIgnoreCountWithQuietUiKey[] =
    "ignore_count_quiet_ui";

// static
const char PermissionDecisionAutoBlocker::kPermissionDismissalEmbargoKey[] =
    "dismissal_embargo_days";

// static
const char PermissionDecisionAutoBlocker::kPermissionIgnoreEmbargoKey[] =
    "ignore_embargo_days";

// static
const char PermissionDecisionAutoBlocker::kPermissionDisplayEmbargoKey[] =
    "display_embargo_minutes";

// static
bool PermissionDecisionAutoBlocker::IsEnabledForContentSetting(
    ContentSettingsType content_setting) {
  return PermissionUtil::IsPermission(content_setting) ||
         content_setting == ContentSettingsType::AUTO_PICTURE_IN_PICTURE ||
         content_setting == ContentSettingsType::FEDERATED_IDENTITY_API ||
         content_setting ==
             ContentSettingsType::FEDERATED_IDENTITY_AUTO_REAUTHN_PERMISSION ||
         content_setting ==
             ContentSettingsType::FILE_SYSTEM_ACCESS_RESTORE_PERMISSION ||
         content_setting == ContentSettingsType::FILE_SYSTEM_WRITE_GUARD ||
         content_setting == ContentSettingsType::SUB_APP_INSTALLATION_PROMPTS
#if BUILDFLAG(IS_CHROMEOS)
         || content_setting == ContentSettingsType::SMART_CARD_GUARD
#endif  // BUILDFLAG(IS_CHROMEOS)
      ;
  // If you add a new content setting here, also add it to
  // GetStringForContentType.
}

// static
std::optional<content::PermissionResult>
PermissionDecisionAutoBlocker::GetEmbargoResult(
    HostContentSettingsMap* settings_map,
    const GURL& request_origin,
    ContentSettingsType permission,
    base::Time current_time) {
  DCHECK(settings_map);
  DCHECK(IsEnabledForContentSetting(permission));

  base::DictValue dict = GetOriginAutoBlockerData(settings_map, request_origin);
  base::DictValue* permission_dict =
      GetOrCreatePermissionDict(dict, GetStringForContentType(permission));

  int dismiss_count = GetActionCount(request_origin, permission,
                                     kPromptDismissCountKey, settings_map);
  if (IsUnderEmbargo(permission_dict, kPermissionDismissalEmbargoKey,
                     current_time,
                     GetEmbargoDurationForContentSettingsType(permission,
                                                              dismiss_count))) {
    return content::PermissionResult(
        PermissionStatus::DENIED,
        content::PermissionStatusSource::MULTIPLE_DISMISSALS);
  }

  if (IsUnderEmbargo(permission_dict, kPermissionIgnoreEmbargoKey, current_time,
                     base::Days(kDefaultEmbargoDays))) {
    return content::PermissionResult(
        PermissionStatus::DENIED,
        content::PermissionStatusSource::MULTIPLE_IGNORES);
  }

  if (IsUnderEmbargo(permission_dict, kPermissionDisplayEmbargoKey,
                     current_time,
                     GetEmbargoDurationForContentSettingsType(
                         permission, /*dismiss_count=*/0))) {
    return content::PermissionResult(
        PermissionStatus::DENIED,
        content::PermissionStatusSource::RECENT_DISPLAY);
  }

  return std::nullopt;
}

bool PermissionDecisionAutoBlocker::IsEmbargoed(
    const GURL& request_origin,
    ContentSettingsType permission) {
  return GetEmbargoResult(request_origin, permission).has_value();
}

std::optional<content::PermissionResult>
PermissionDecisionAutoBlocker::GetEmbargoResult(
    const GURL& request_origin,
    ContentSettingsType permission) {
  return GetEmbargoResult(settings_map_, request_origin, permission,
                          clock_->Now());
}

base::Time PermissionDecisionAutoBlocker::GetEmbargoStartTime(
    const GURL& request_origin,
    ContentSettingsType permission) {
  DCHECK(settings_map_);
  base::DictValue dict =
      GetOriginAutoBlockerData(settings_map_, request_origin);
  base::DictValue* permission_dict =
      GetOrCreatePermissionDict(dict, GetStringForContentType(permission));

  // A permission may have a record for both dismisal and ignore, return the
  // most recent. A permission will only actually be under one embargo, but
  // the record of embargo start will persist until explicitly deleted
  base::Time dismissal_start_time = permissions::GetEmbargoStartTime(
      permission_dict, kPermissionDismissalEmbargoKey);
  base::Time ignore_start_time = permissions::GetEmbargoStartTime(
      permission_dict, kPermissionIgnoreEmbargoKey);

  return dismissal_start_time > ignore_start_time ? dismissal_start_time
                                                  : ignore_start_time;
}

std::set<GURL> PermissionDecisionAutoBlocker::GetEmbargoedOrigins(
    ContentSettingsType content_type) {
  return GetEmbargoedOrigins(std::vector<ContentSettingsType>{content_type});
}

std::set<GURL> PermissionDecisionAutoBlocker::GetEmbargoedOrigins(
    std::vector<ContentSettingsType> content_types) {
  DCHECK(settings_map_);

  std::vector<ContentSettingsType> filtered_content_types;
  for (ContentSettingsType content_type : content_types) {
    if (IsEnabledForContentSetting(content_type)) {
      filtered_content_types.emplace_back(content_type);
    }
  }
  if (filtered_content_types.empty()) {
    return std::set<GURL>();
  }

  std::set<GURL> origins;
  for (const auto& e : settings_map_->GetSettingsForOneType(
           ContentSettingsType::PERMISSION_AUTOBLOCKER_DATA)) {
    for (auto content_type : filtered_content_types) {
      const GURL url(e.primary_pattern.ToString());
      if (IsEmbargoed(url, content_type)) {
        origins.insert(url);
        break;
      }
    }
  }
  return origins;
}

int PermissionDecisionAutoBlocker::GetDismissCount(
    const GURL& url,
    ContentSettingsType permission) {
  return GetActionCount(url, permission, kPromptDismissCountKey, settings_map_);
}

int PermissionDecisionAutoBlocker::GetIgnoreCount(
    const GURL& url,
    ContentSettingsType permission) {
  return GetActionCount(url, permission, kPromptIgnoreCountKey, settings_map_);
}

bool PermissionDecisionAutoBlocker::RecordDismissAndEmbargo(
    const GURL& url,
    ContentSettingsType permission,
    bool dismissed_prompt_was_quiet) {
  int current_dismissal_count = RecordActionInWebsiteSettings(
      url, permission, kPromptDismissCountKey, settings_map_);

  int current_dismissal_count_with_quiet_ui =
      dismissed_prompt_was_quiet
          ? RecordActionInWebsiteSettings(url, permission,
                                          kPromptDismissCountWithQuietUiKey,
                                          settings_map_)
          : -1;

  // TODO(dominickn): ideally we would have a method
  // ContentSettingPermissionContextBase::ShouldEmbargoAfterRepeatedDismissals()
  // to specify if a permission is opted in. This is difficult right now
  // because:
  // 1. PermissionQueueController needs to call this method at a point where it
  //    does not have a ContentSettingPermissionContextBase available
  // 2. Not calling RecordDismissAndEmbargo means no repeated dismissal metrics
  //    are recorded
  if (current_dismissal_count >=
      GetDismissalsBeforeBlockForContentSettingsType(permission)) {
    PlaceUnderEmbargo(url, permission, kPermissionDismissalEmbargoKey);
    return true;
  }

  if (current_dismissal_count_with_quiet_ui >=
      kDefaultDismissalsBeforeBlockWithQuietUi) {
    DCHECK(permission == ContentSettingsType::NOTIFICATIONS ||
           permission == ContentSettingsType::GEOLOCATION ||
           permission == ContentSettingsType::GEOLOCATION_WITH_OPTIONS);
    PlaceUnderEmbargo(url, permission, kPermissionDismissalEmbargoKey);
    return true;
  }
  return false;
}

bool PermissionDecisionAutoBlocker::RecordIgnoreAndEmbargo(
    const GURL& url,
    ContentSettingsType permission,
    bool ignored_prompt_was_quiet) {
  int current_ignore_count = RecordActionInWebsiteSettings(
      url, permission, kPromptIgnoreCountKey, settings_map_);

  int current_ignore_count_with_quiet_ui =
      ignored_prompt_was_quiet
          ? RecordActionInWebsiteSettings(url, permission,
                                          kPromptIgnoreCountWithQuietUiKey,
                                          settings_map_)
          : -1;

  int ignores_before_block = kDefaultIgnoresBeforeBlock;
#if BUILDFLAG(IS_ANDROID)
  if (base::FeatureList::IsEnabled(
          permissions::kPermissionsAndroidClapperLoud) &&
      permission == ContentSettingsType::NOTIFICATIONS &&
      !ignored_prompt_was_quiet) {
    ignores_before_block = kClapperIgnoresBeforeBlock;
  }
#endif

  if (current_ignore_count >= ignores_before_block) {
    PlaceUnderEmbargo(url, permission, kPermissionIgnoreEmbargoKey);
    return true;
  }

  if (current_ignore_count_with_quiet_ui >=
      kDefaultIgnoresBeforeBlockWithQuietUi) {
    DCHECK(permission == ContentSettingsType::NOTIFICATIONS ||
           permission == permissions::PermissionUtil::GetGeolocationType());
    PlaceUnderEmbargo(url, permission, kPermissionIgnoreEmbargoKey);
    return true;
  }

  return false;
}

bool PermissionDecisionAutoBlocker::RecordDisplayAndEmbargo(
    const GURL& url,
    ContentSettingsType permission) {
  DCHECK_EQ(permission,
            ContentSettingsType::FEDERATED_IDENTITY_AUTO_REAUTHN_PERMISSION);
  PlaceUnderEmbargo(url, permission, kPermissionDisplayEmbargoKey);
  return true;
}

void PermissionDecisionAutoBlocker::RemoveEmbargoAndResetCounts(
    const GURL& url,
    ContentSettingsType permission) {
  if (!IsEnabledForContentSetting(permission)) {
    return;
  }

  base::DictValue dict = GetOriginAutoBlockerData(settings_map_, url);

  dict.Remove(GetStringForContentType(permission));

  settings_map_->SetWebsiteSettingDefaultScope(
      url, GURL(), ContentSettingsType::PERMISSION_AUTOBLOCKER_DATA,
      base::Value(std::move(dict)));
}

void PermissionDecisionAutoBlocker::RemoveEmbargoAndResetCounts(
    base::RepeatingCallback<bool(const GURL& url)> filter) {
  for (const auto& site : settings_map_->GetSettingsForOneType(
           ContentSettingsType::PERMISSION_AUTOBLOCKER_DATA)) {
    GURL origin(site.primary_pattern.ToString());

    if (origin.is_valid() && filter.Run(origin)) {
      settings_map_->SetWebsiteSettingDefaultScope(
          origin, GURL(), ContentSettingsType::PERMISSION_AUTOBLOCKER_DATA,
          base::Value());
    }
  }
}

void PermissionDecisionAutoBlocker::AddObserver(Observer* obs) {
  observers_.AddObserver(obs);
}

void PermissionDecisionAutoBlocker::RemoveObserver(Observer* obs) {
  observers_.RemoveObserver(obs);
}

// static
const char*
PermissionDecisionAutoBlocker::GetPromptDismissCountKeyForTesting() {
  return kPromptDismissCountKey;
}

PermissionDecisionAutoBlocker::PermissionDecisionAutoBlocker(
    HostContentSettingsMap* settings_map)
    : settings_map_(settings_map), clock_(base::DefaultClock::GetInstance()) {}

PermissionDecisionAutoBlocker::~PermissionDecisionAutoBlocker() = default;

void PermissionDecisionAutoBlocker::PlaceUnderEmbargo(
    const GURL& request_origin,
    ContentSettingsType permission,
    const char* key) {
  base::DictValue dict =
      GetOriginAutoBlockerData(settings_map_, request_origin);
  base::DictValue* permission_dict =
      GetOrCreatePermissionDict(dict, GetStringForContentType(permission));
  permission_dict->Set(
      key, base::Value(static_cast<double>(clock_->Now().ToInternalValue())));
  settings_map_->SetWebsiteSettingDefaultScope(
      request_origin, GURL(), ContentSettingsType::PERMISSION_AUTOBLOCKER_DATA,
      base::Value(std::move(dict)));
  NotifyEmbargoStarted(request_origin, permission);
}

void PermissionDecisionAutoBlocker::NotifyEmbargoStarted(
    const GURL& origin,
    ContentSettingsType content_setting) {
  for (Observer& obs : observers_) {
    obs.OnEmbargoStarted(origin, content_setting);
  }
}

void PermissionDecisionAutoBlocker::SetClockForTesting(base::Clock* clock) {
  clock_ = clock;
}

}  // namespace permissions
