blob: 41dc09cf1a99957d033171708e91d5caee95f8c5 [file] [log] [blame]
// 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 "components/permissions/permission_actions_history.h"
#include <algorithm>
#include <optional>
#include <string_view>
#include <vector>
#include "base/containers/adapters.h"
#include "base/feature_list.h"
#include "base/json/values_util.h"
#include "base/values.h"
#include "build/build_config.h"
#include "components/keyed_service/content/browser_context_dependency_manager.h"
#include "components/permissions/features.h"
#include "components/permissions/permission_util.h"
#include "components/permissions/pref_names.h"
#include "components/permissions/request_type.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "third_party/blink/public/common/features.h"
namespace permissions {
namespace {
// Inner structure of |prefs::kPermissionActions| containing a history of past
// permission actions. It is a dictionary of JSON lists keyed on the result of
// PermissionUtil::GetPermissionString (lower-cased for backwards compatibility)
// and has the following format:
//
// "profile.content_settings.permission_actions": {
// "notifications": [
// { "time": "1333333333337", "action": 1, "prompt_disposition": 2 },
// { "time": "1567957177000", "action": 3, "prompt_disposition": 4 },
// ],
// "geolocation": [...],
// ...
// }
// The "prompt_disposition" key was added in M96. Any older entry will be
// missing that key. The value is backed by the PermissionPromptDisposition
// enum.
//
// Website settings are used to store data related to heuristic grants. This
// data is stored per-origin and per-permission type, and includes:
// - kTemporaryGrantCountKey: Stores the number of heuristic temporary grants
// for a permission.
// - kTemporaryGrantTimeStampKey: Stores the timestamp of the most recent
// temporary grant, including auto-grants.
constexpr char kPermissionActionEntryActionKey[] = "action";
constexpr char kPermissionActionEntryTimestampKey[] = "time";
constexpr char kPermissionActionEntryPromptDispositionKey[] =
"prompt_disposition";
// Entries in permission actions expire after they become this old.
constexpr base::TimeDelta kPermissionActionMaxAge = base::Days(90);
// The threshold for temporary grants before a heuristic grant is made.
constexpr int kHeuristicGrantThreshold = 3;
// The duration after which the auto-grant expires.
constexpr base::TimeDelta kAutoGrantHeuristicallyExpiration = base::Days(28);
// Keys for storing data in website settings.
constexpr char kTemporaryGrantCountKey[] = "temporary_grant_count";
constexpr char kTemporaryGrantTimeStampKey[] = "temporary_grant_time_days";
base::Value::Dict GetOriginActionHistoryData(HostContentSettingsMap* settings,
const GURL& origin_url) {
base::Value website_setting = settings->GetWebsiteSetting(
origin_url, GURL(), ContentSettingsType::PERMISSION_ACTIONS_HISTORY);
if (!website_setting.is_dict()) {
return base::Value::Dict();
}
return std::move(website_setting.GetDict());
}
base::Value::Dict* EnsurePermissionDict(base::Value::Dict& origin_dict,
ContentSettingsType content_type) {
// TODO(crbug.com/450467541): Support approximate location.
CHECK((content_type == ContentSettingsType::GEOLOCATION ||
content_type == ContentSettingsType::GEOLOCATION_WITH_OPTIONS) &&
base::FeatureList::IsEnabled(blink::features::kGeolocationElement) &&
base::FeatureList::IsEnabled(
permissions::features::kPermissionHeuristicAutoGrant));
return origin_dict.EnsureDict(
PermissionUtil::GetPermissionString(content_type));
}
// Returns the current number of temporary grants recorded for `permission`
// type at `url`.
int GetTemporaryGrantCount(const GURL& url,
ContentSettingsType permission,
HostContentSettingsMap* settings_map) {
base::Value::Dict dict = GetOriginActionHistoryData(settings_map, url);
base::Value::Dict* permission_dict = EnsurePermissionDict(dict, permission);
std::optional<int> value = permission_dict->FindInt(kTemporaryGrantCountKey);
return value.value_or(0);
}
} // namespace
PermissionActionsHistory::PermissionActionsHistory(
PrefService* pref_service,
HostContentSettingsMap* settings_map)
: pref_service_(pref_service), settings_map_(settings_map) {}
PermissionActionsHistory::~PermissionActionsHistory() = default;
std::vector<PermissionActionsHistory::Entry>
PermissionActionsHistory::GetHistory(const base::Time& begin,
EntryFilter entry_filter) {
const base::Value::Dict& dictionary =
pref_service_->GetDict(prefs::kPermissionActions);
std::vector<PermissionActionsHistory::Entry> matching_actions;
for (auto permission_entry : dictionary) {
const auto permission_actions =
GetHistoryInternal(begin, permission_entry.first, entry_filter);
matching_actions.insert(matching_actions.end(), permission_actions.begin(),
permission_actions.end());
}
std::ranges::sort(
matching_actions, {},
[](const PermissionActionsHistory::Entry& entry) { return entry.time; });
return matching_actions;
}
std::vector<PermissionActionsHistory::Entry>
PermissionActionsHistory::GetHistory(const base::Time& begin,
RequestType type,
EntryFilter entry_filter) {
return GetHistoryInternal(begin, PermissionKeyForRequestType(type),
entry_filter);
}
void PermissionActionsHistory::RecordAction(
PermissionAction action,
RequestType type,
PermissionPromptDisposition prompt_disposition) {
ScopedDictPrefUpdate update(pref_service_, prefs::kPermissionActions);
base::Value::Dict& update_dict = update.Get();
const std::string_view permission_path(PermissionKeyForRequestType(type));
if (!update_dict.FindListByDottedPath(permission_path)) {
update_dict.SetByDottedPath(permission_path, base::Value::List());
}
base::Value::List* permission_actions =
update_dict.FindListByDottedPath(permission_path);
CHECK(permission_actions);
// Discard permission actions older than |kPermissionActionMaxAge|.
const base::Time cutoff = base::Time::Now() - kPermissionActionMaxAge;
permission_actions->EraseIf([cutoff](const base::Value& entry) {
const std::optional<base::Time> timestamp = base::ValueToTime(
entry.GetDict().Find(kPermissionActionEntryTimestampKey));
return !timestamp || *timestamp < cutoff;
});
// Record the new permission action.
base::Value::Dict new_action_attributes;
new_action_attributes.Set(kPermissionActionEntryTimestampKey,
base::TimeToValue(base::Time::Now()));
new_action_attributes.Set(kPermissionActionEntryActionKey,
static_cast<int>(action));
new_action_attributes.Set(kPermissionActionEntryPromptDispositionKey,
static_cast<int>(prompt_disposition));
permission_actions->Append(std::move(new_action_attributes));
}
void PermissionActionsHistory::ClearHistory(const base::Time& delete_begin,
const base::Time& delete_end) {
DCHECK(!delete_end.is_null());
if (delete_begin.is_null() && delete_end.is_max()) {
pref_service_->ClearPref(prefs::kPermissionActions);
return;
}
ScopedDictPrefUpdate update(pref_service_, prefs::kPermissionActions);
for (auto permission_entry : update.Get()) {
permission_entry.second.GetList().EraseIf([delete_begin,
delete_end](const auto& entry) {
const std::optional<base::Time> timestamp = base::ValueToTime(
entry.GetDict().Find(kPermissionActionEntryTimestampKey));
return (!timestamp ||
(*timestamp >= delete_begin && *timestamp < delete_end));
});
}
}
bool PermissionActionsHistory::RecordTemporaryGrant(
const GURL& url,
ContentSettingsType permission) {
base::Value::Dict dict = GetOriginActionHistoryData(settings_map_, url);
base::Value::Dict* permission_dict = EnsurePermissionDict(dict, permission);
std::optional<int> value = permission_dict->FindInt(kTemporaryGrantCountKey);
int current_count = value.value_or(0);
permission_dict->Set(kTemporaryGrantCountKey, current_count + 1);
permission_dict->Set(kTemporaryGrantTimeStampKey,
base::TimeToValue(base::Time::Now()));
settings_map_->SetWebsiteSettingDefaultScope(
url, GURL(), ContentSettingsType::PERMISSION_ACTIONS_HISTORY,
base::Value(std::move(dict)));
bool auto_granted = current_count >= kHeuristicGrantThreshold;
if (auto_granted) {
NotifyAutoGrantedHeuristically(url, permission);
}
return auto_granted;
}
void PermissionActionsHistory::ResetHeuristicData(
const GURL& url,
ContentSettingsType permission) {
base::Value::Dict dict = GetOriginActionHistoryData(settings_map_, url);
if (dict.empty()) {
return;
}
dict.Remove(PermissionUtil::GetPermissionString(permission));
settings_map_->SetWebsiteSettingDefaultScope(
url, GURL(), ContentSettingsType::PERMISSION_ACTIONS_HISTORY,
base::Value(std::move(dict)));
}
void PermissionActionsHistory::ResetHeuristicData(
base::RepeatingCallback<bool(const GURL& url)> filter) {
for (const auto& site : settings_map_->GetSettingsForOneType(
ContentSettingsType::PERMISSION_ACTIONS_HISTORY)) {
GURL origin(site.primary_pattern.ToString());
if (origin.is_valid() && filter.Run(origin)) {
settings_map_->SetWebsiteSettingDefaultScope(
origin, GURL(), ContentSettingsType::PERMISSION_ACTIONS_HISTORY,
base::Value());
}
}
}
bool PermissionActionsHistory::CheckHeuristicallyAutoGranted(
const GURL& request_origin,
ContentSettingsType permission,
bool needs_update) {
base::Value::Dict dict =
GetOriginActionHistoryData(settings_map_, request_origin);
base::Value::Dict* permission_dict = EnsurePermissionDict(dict, permission);
std::optional<base::Time> last_grant_time =
base::ValueToTime(permission_dict->Find(kTemporaryGrantTimeStampKey));
std::optional<int> grant_count_opt =
permission_dict->FindInt(kTemporaryGrantCountKey);
int grant_count = grant_count_opt.value_or(0);
// Check if the last grant has expired. If the grant has expired, decay the
// count. If the count was at or above the threshold, it decays to 2.
// Otherwise, the heuristic data is reset completely.
if (last_grant_time.has_value() && base::Time::Now() - *last_grant_time >
kAutoGrantHeuristicallyExpiration) {
if (grant_count >= kHeuristicGrantThreshold) {
permission_dict->Set(kTemporaryGrantCountKey,
kHeuristicGrantThreshold - 1);
permission_dict->Set(kTemporaryGrantTimeStampKey,
base::TimeToValue(base::Time::Now()));
settings_map_->SetWebsiteSettingDefaultScope(
request_origin, GURL(),
ContentSettingsType::PERMISSION_ACTIONS_HISTORY,
base::Value(std::move(dict)));
} else {
ResetHeuristicData(request_origin, permission);
}
return false;
}
if (grant_count >= kHeuristicGrantThreshold) {
if (needs_update) {
permission_dict->Set(kTemporaryGrantTimeStampKey,
base::TimeToValue(base::Time::Now()));
settings_map_->SetWebsiteSettingDefaultScope(
request_origin, GURL(),
ContentSettingsType::PERMISSION_ACTIONS_HISTORY,
base::Value(std::move(dict)));
}
return true;
}
// The grant count is below the threshold, so it is not auto-granted.
return false;
}
void PermissionActionsHistory::AddObserver(Observer* obs) {
observers_.AddObserver(obs);
}
void PermissionActionsHistory::RemoveObserver(Observer* obs) {
observers_.RemoveObserver(obs);
}
int PermissionActionsHistory::GetTemporaryGrantCountForTesting(
const GURL& request_origin,
ContentSettingsType permission) {
return GetTemporaryGrantCount(request_origin, permission, settings_map_);
}
std::vector<PermissionActionsHistory::Entry>
PermissionActionsHistory::GetHistoryInternal(const base::Time& begin,
const std::string& key,
EntryFilter entry_filter) {
const base::Value::List* permission_actions =
pref_service_->GetDict(prefs::kPermissionActions).FindList(key);
if (!permission_actions)
return {};
std::vector<Entry> matching_actions;
for (const auto& entry : *permission_actions) {
const base::Value::Dict& entry_dict = entry.GetDict();
const std::optional<base::Time> timestamp =
base::ValueToTime(entry_dict.Find(kPermissionActionEntryTimestampKey));
if (timestamp < begin)
continue;
if (entry_filter != EntryFilter::WANT_ALL_PROMPTS) {
// If we want either the Loud or Quiet UI actions but don't have this
// info due to legacy reasons we ignore the entry.
const std::optional<int> prompt_disposition_int =
entry_dict.FindInt(kPermissionActionEntryPromptDispositionKey);
if (!prompt_disposition_int)
continue;
const PermissionPromptDisposition prompt_disposition =
static_cast<PermissionPromptDisposition>(*prompt_disposition_int);
if (entry_filter == EntryFilter::WANT_LOUD_PROMPTS_ONLY &&
!PermissionUmaUtil::IsPromptDispositionLoud(prompt_disposition)) {
continue;
}
if (entry_filter == EntryFilter::WANT_QUIET_PROMPTS_ONLY &&
!PermissionUmaUtil::IsPromptDispositionQuiet(prompt_disposition)) {
continue;
}
}
const PermissionAction past_action = static_cast<PermissionAction>(
*(entry_dict.FindInt(kPermissionActionEntryActionKey)));
matching_actions.emplace_back(
PermissionActionsHistory::Entry{past_action, timestamp.value()});
}
return matching_actions;
}
void PermissionActionsHistory::NotifyAutoGrantedHeuristically(
const GURL& origin,
ContentSettingsType content_setting) {
for (Observer& obs : observers_) {
obs.OnAutoGrantedHeuristically(origin, content_setting);
}
}
PrefService* PermissionActionsHistory::GetPrefServiceForTesting() {
return pref_service_;
}
// static
void PermissionActionsHistory::FillInActionCounts(
PredictionRequestFeatures::ActionCounts* counts,
const std::vector<PermissionActionsHistory::Entry>& actions) {
for (const auto& entry : actions) {
switch (entry.action) {
case PermissionAction::DENIED:
counts->denies++;
break;
case PermissionAction::GRANTED:
case PermissionAction::GRANTED_ONCE:
counts->grants++;
break;
case PermissionAction::DISMISSED:
counts->dismissals++;
break;
case PermissionAction::IGNORED:
counts->ignores++;
break;
default:
// Anything else is ignored.
break;
}
}
}
// static
void PermissionActionsHistory::RegisterProfilePrefs(
user_prefs::PrefRegistrySyncable* registry) {
registry->RegisterDictionaryPref(prefs::kPermissionActions,
PrefRegistry::LOSSY_PREF);
}
} // namespace permissions