#include "chrome/browser/ui/safety_hub/unused_site_permissions_manager.h"
#include <memory>
#include <optional>
#include <set>
#include <utility>
#include <vector>
#include "base/check.h"
#include "base/check_op.h"
#include "base/feature_list.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/string_number_conversions.h"
#include "base/time/clock.h"
#include "base/time/default_clock.h"
#include "base/time/time.h"
#include "base/values.h"
#include "chrome/browser/ui/safety_hub/revoked_permissions_result.h"
#include "chrome/browser/ui/safety_hub/safety_hub_prefs.h"
#include "chrome/browser/ui/safety_hub/safety_hub_result.h"
#include "chrome/browser/ui/safety_hub/safety_hub_util.h"
#include "components/content_settings/core/browser/content_settings_info.h"
#include "components/content_settings/core/browser/content_settings_registry.h"
#include "components/content_settings/core/browser/content_settings_uma_util.h"
#include "components/content_settings/core/browser/content_settings_utils.h"
#include "components/content_settings/core/browser/website_settings_registry.h"
#include "components/content_settings/core/common/content_settings.h"
#include "components/content_settings/core/common/content_settings_constraints.h"
#include "components/content_settings/core/common/content_settings_pattern.h"
#include "components/content_settings/core/common/content_settings_types.h"
#include "components/content_settings/core/common/features.h"
#include "components/permissions/constants.h"
#include "components/permissions/permission_uma_util.h"
#include "components/prefs/pref_change_registrar.h"
#include "components/prefs/pref_service.h"
#include "components/safety_check/safety_check.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "url/gurl.h"
#include "url/origin.h"
constexpr base::TimeDelta kRevocationThresholdNoDelayForTesting = base::Days(0);
constexpr base::TimeDelta kRevocationThresholdWithDelayForTesting =
base::Minutes(5);
namespace {
constexpr char kUnknownContentSettingsType[] = "unknown";
constexpr size_t kAllowAgainMetricsExclusiveMaxCount = 31;
constexpr size_t kAllowAgainMetricsBuckets = 31;
const base::TimeDelta kUnusedSitePermissionsRevocationThreshold =
base::Days(60);
base::TimeDelta GetRevocationThreshold() {
if (content_settings::features::kSafetyCheckUnusedSitePermissionsNoDelay
.Get()) {
return kRevocationThresholdNoDelayForTesting;
} else if (content_settings::features::
kSafetyCheckUnusedSitePermissionsWithDelay.Get()) {
return kRevocationThresholdWithDelayForTesting;
}
return kUnusedSitePermissionsRevocationThreshold;
}
bool IsContentSetting(ContentSettingsType type) {
auto* content_setting_registry =
content_settings::ContentSettingsRegistry::GetInstance();
return content_setting_registry->Get(type);
}
bool IsWebsiteSetting(ContentSettingsType type) {
auto* website_setting_registry =
content_settings::WebsiteSettingsRegistry::GetInstance();
return website_setting_registry->Get(type);
}
bool IsChooserPermissionSupported() {
return base::FeatureList::IsEnabled(
content_settings::features::
kSafetyCheckUnusedSitePermissionsForSupportedChooserPermissions);
}
const std::set<ContentSettingsType> GetRevokedUnusedSitePermissionTypes(
const std::set<ContentSettingsType> permissions) {
std::set<ContentSettingsType>
permissions_without_revoked_abusive_notification_manager = permissions;
if (permissions_without_revoked_abusive_notification_manager.contains(
ContentSettingsType::NOTIFICATIONS)) {
permissions_without_revoked_abusive_notification_manager.erase(
ContentSettingsType::NOTIFICATIONS);
}
return permissions_without_revoked_abusive_notification_manager;
}
base::Value::List ConvertContentSettingsIntValuesToString(
const base::Value::List& content_settings_values_list,
bool* successful_migration) {
base::Value::List string_value_list;
for (const base::Value& setting_value : content_settings_values_list) {
if (setting_value.is_int()) {
int setting_int = setting_value.GetInt();
auto setting_name =
UnusedSitePermissionsManager::ConvertContentSettingsTypeToKey(
static_cast<ContentSettingsType>(setting_int));
if (setting_name == kUnknownContentSettingsType) {
*successful_migration = false;
string_value_list.Append(setting_value.GetInt());
} else {
string_value_list.Append(setting_name);
}
} else {
CHECK(setting_value.is_string());
string_value_list.Append(setting_value.GetString());
}
}
return string_value_list;
}
base::Value::Dict ConvertChooserContentSettingsIntValuesToString(
const base::Value::Dict& chooser_content_settings_values_dict) {
base::Value::Dict string_keyed_dict;
for (const auto [key, value] : chooser_content_settings_values_dict) {
int number = -1;
base::StringToInt(key, &number);
if (number == 0) {
string_keyed_dict.Set(key, value.GetDict().Clone());
} else {
string_keyed_dict.Set(
UnusedSitePermissionsManager::ConvertContentSettingsTypeToKey(
static_cast<ContentSettingsType>(number)),
value.GetDict().Clone());
}
}
return string_keyed_dict;
}
}
std::unique_ptr<SafetyHubResult>
UnusedSitePermissionsManager::UpdateOnBackgroundThread(
base::Clock* clock,
const scoped_refptr<HostContentSettingsMap> hcsm) {
UnusedSitePermissionsManager::UnusedPermissionMap recently_unused;
const base::Time threshold =
clock->Now() - content_settings::GetCoarseVisitedTimePrecision();
auto* website_setting_registry =
content_settings::WebsiteSettingsRegistry::GetInstance();
for (const content_settings::WebsiteSettingsInfo* info :
*website_setting_registry) {
ContentSettingsType type = info->type();
if (!IsContentSetting(type) && IsWebsiteSetting(type) &&
!IsChooserPermissionSupported()) {
continue;
}
if (!content_settings::CanTrackLastVisit(type)) {
continue;
}
ContentSettingsForOneType settings = hcsm->GetSettingsForOneType(type);
for (const auto& setting : settings) {
if (!setting.primary_pattern.MatchesSingleOrigin()) {
continue;
}
if (setting.metadata.last_visited() != base::Time() &&
setting.metadata.last_visited() < threshold) {
url::Origin origin =
ConvertPrimaryPatternToOrigin(setting.primary_pattern);
recently_unused[origin.Serialize()].push_back(
{type, std::move(setting)});
}
}
}
auto result = std::make_unique<RevokedPermissionsResult>();
result->SetRecentlyUnusedPermissions(recently_unused);
return std::move(result);
}
std::string UnusedSitePermissionsManager::ConvertContentSettingsTypeToKey(
ContentSettingsType type) {
auto* website_setting_registry =
content_settings::WebsiteSettingsRegistry::GetInstance();
CHECK(website_setting_registry);
auto* website_settings_info = website_setting_registry->Get(type);
if (!website_settings_info) {
auto integer_type = static_cast<int32_t>(type);
DVLOG(1) << "Couldn't retrieve website settings info entry from the "
"registry for type: "
<< integer_type;
base::UmaHistogramSparse(
"Settings.SafetyCheck.UnusedSitePermissionsMigrationFail",
integer_type);
return kUnknownContentSettingsType;
}
return website_settings_info->name();
}
ContentSettingsType
UnusedSitePermissionsManager::ConvertKeyToContentSettingsType(
const std::string& key) {
auto* website_setting_registry =
content_settings::WebsiteSettingsRegistry::GetInstance();
return website_setting_registry->GetByName(key)->type();
}
url::Origin UnusedSitePermissionsManager::ConvertPrimaryPatternToOrigin(
const ContentSettingsPattern& primary_pattern) {
GURL origin_url = GURL(primary_pattern.ToString());
CHECK(origin_url.is_valid());
return url::Origin::Create(origin_url);
}
UnusedSitePermissionsManager::UnusedSitePermissionsManager(
content::BrowserContext* browser_context,
PrefService* prefs)
: browser_context_(browser_context),
clock_(base::DefaultClock::GetInstance()) {
CHECK_CURRENTLY_ON(content::BrowserThread::UI);
CHECK(browser_context_);
pref_change_registrar_ = std::make_unique<PrefChangeRegistrar>();
pref_change_registrar_->Init(prefs);
bool migration_completed = pref_change_registrar_->prefs()->GetBoolean(
safety_hub_prefs::kUnusedSitePermissionsRevocationMigrationCompleted);
if (!migration_completed) {
UpdateIntegerValuesToGroupName();
}
}
UnusedSitePermissionsManager::~UnusedSitePermissionsManager() = default;
void UnusedSitePermissionsManager::RevokeUnusedPermissions(
std::unique_ptr<SafetyHubResult> result) {
is_unused_site_revocation_running_ = true;
auto* interim_result = static_cast<RevokedPermissionsResult*>(result.get());
recently_unused_permissions_ = interim_result->GetRecentlyUnusedPermissions();
base::Time threshold = clock_->Now() - GetRevocationThreshold();
for (auto itr = recently_unused_permissions_.begin();
itr != recently_unused_permissions_.end();) {
std::list<ContentSettingEntry>& unused_site_permissions = itr->second;
ContentSettingsPattern primary_pattern =
unused_site_permissions.front().source.primary_pattern;
ContentSettingsPattern secondary_pattern =
unused_site_permissions.front().source.secondary_pattern;
std::set<ContentSettingsType> revoked_permissions;
base::Value::Dict chooser_permissions_data;
for (auto permission_itr = unused_site_permissions.begin();
permission_itr != unused_site_permissions.end();) {
const ContentSettingEntry& entry = *permission_itr;
if (!content_settings::CanBeAutoRevokedAsUnusedPermission(
entry.type, entry.source.setting_value)) {
permission_itr++;
continue;
}
CHECK_EQ(entry.source.primary_pattern, primary_pattern);
CHECK(entry.source.secondary_pattern ==
ContentSettingsPattern::Wildcard() ||
entry.source.secondary_pattern == entry.source.primary_pattern);
CHECK_NE(entry.source.metadata.last_visited(), base::Time());
CHECK(entry.type != ContentSettingsType::NOTIFICATIONS);
if (entry.source.metadata.last_visited() < threshold &&
entry.source.secondary_pattern ==
ContentSettingsPattern::Wildcard()) {
permissions::PermissionUmaUtil::ScopedRevocationReporter reporter(
browser_context_.get(), entry.source.primary_pattern,
entry.source.secondary_pattern, entry.type,
permissions::PermissionSourceUI::SAFETY_HUB_AUTO_REVOCATION);
content_settings_uma_util::RecordContentSettingsHistogram(
"Settings.SafetyHub.UnusedSitePermissionsModule.AutoRevoked2",
entry.type);
revoked_permissions.insert(entry.type);
if (IsContentSetting(entry.type)) {
hcsm()->SetContentSettingCustomScope(
entry.source.primary_pattern, entry.source.secondary_pattern,
entry.type, ContentSetting::CONTENT_SETTING_DEFAULT);
} else if (IsChooserPermissionSupported() &&
IsWebsiteSetting(entry.type)) {
chooser_permissions_data.Set(
ConvertContentSettingsTypeToKey(entry.type),
entry.source.setting_value.Clone());
hcsm()->SetWebsiteSettingCustomScope(entry.source.primary_pattern,
entry.source.secondary_pattern,
entry.type, base::Value());
} else {
NOTREACHED()
<< "Unable to find ContentSettingsType in neither "
<< "ContentSettingsRegistry nor WebsiteSettingsRegistry: "
<< ConvertContentSettingsTypeToKey(entry.type);
}
unused_site_permissions.erase(permission_itr++);
} else {
permission_itr++;
}
}
if (!revoked_permissions.empty()) {
StorePermissionInUnusedSitePermissionSetting(
revoked_permissions, chooser_permissions_data, std::nullopt,
primary_pattern, secondary_pattern);
}
if (unused_site_permissions.empty()) {
recently_unused_permissions_.erase(itr++);
} else {
itr++;
}
}
is_unused_site_revocation_running_ = false;
}
bool UnusedSitePermissionsManager::IsRevocationRunning() {
return is_unused_site_revocation_running_;
}
void UnusedSitePermissionsManager::OnPageVisited(const url::Origin& origin) {
CHECK_CURRENTLY_ON(content::BrowserThread::UI);
auto origin_entry = recently_unused_permissions_.find(origin.Serialize());
if (origin_entry == recently_unused_permissions_.end()) {
return;
}
auto& site_permissions = origin_entry->second;
for (auto it = site_permissions.begin(); it != site_permissions.end();) {
if (it->source.primary_pattern.Matches(origin.GetURL())) {
hcsm()->UpdateLastVisitedTime(it->source.primary_pattern,
it->source.secondary_pattern, it->type);
site_permissions.erase(it++);
} else {
it++;
}
}
if (site_permissions.empty()) {
recently_unused_permissions_.erase(origin_entry);
}
}
void UnusedSitePermissionsManager::RegrantPermissionsForOrigin(
const url::Origin& origin) {
content_settings::SettingInfo info;
base::Value stored_value(hcsm()->GetWebsiteSetting(
origin.GetURL(), origin.GetURL(),
ContentSettingsType::REVOKED_UNUSED_SITE_PERMISSIONS, &info));
if (!stored_value.is_dict()) {
return;
}
base::Value::List* permission_type_list =
stored_value.GetDict().FindList(permissions::kRevokedKey);
CHECK(permission_type_list);
is_unused_site_revocation_running_ = true;
for (auto& permission_type : *permission_type_list) {
ContentSettingsType type =
ConvertKeyToContentSettingsType(permission_type.GetString());
if (IsContentSetting(type)) {
hcsm()->SetContentSettingCustomScope(
info.primary_pattern, info.secondary_pattern, type,
ContentSetting::CONTENT_SETTING_ALLOW);
} else if (IsChooserPermissionSupported() && IsWebsiteSetting(type)) {
auto* chooser_permissions_data = stored_value.GetDict().FindDict(
permissions::kRevokedChooserPermissionsKey);
CHECK(chooser_permissions_data);
auto* revoked_value = chooser_permissions_data->FindDict(
ConvertContentSettingsTypeToKey(type));
CHECK(revoked_value);
hcsm()->SetWebsiteSettingCustomScope(
info.primary_pattern, info.secondary_pattern, type,
base::Value(std::move(*revoked_value)));
} else {
NOTREACHED() << "Unable to find ContentSettingsType in neither "
<< "ContentSettingsRegistry nor WebsiteSettingsRegistry: "
<< ConvertContentSettingsTypeToKey(type);
}
}
is_unused_site_revocation_running_ = false;
IgnoreOriginForAutoRevocation(origin);
DeletePatternFromRevokedUnusedSitePermissionList(info.primary_pattern,
info.secondary_pattern);
base::Time revoked_time =
info.metadata.expiration() -
safety_check::GetUnusedSitePermissionsRevocationCleanUpThreshold();
base::UmaHistogramCustomCounts(
"Settings.SafetyCheck.UnusedSitePermissionsAllowAgainDays",
(clock_->Now() - revoked_time).InDays(), 0,
kAllowAgainMetricsExclusiveMaxCount, kAllowAgainMetricsBuckets);
}
void UnusedSitePermissionsManager::UndoRegrantPermissionsForOrigin(
const PermissionsData& permissions_data) {
const std::set<ContentSettingsType> unused_site_permission_types =
GetRevokedUnusedSitePermissionTypes(permissions_data.permission_types);
if (unused_site_permission_types.empty()) {
return;
}
is_unused_site_revocation_running_ = true;
for (const auto& permission : unused_site_permission_types) {
if (IsContentSetting(permission)) {
hcsm()->SetContentSettingCustomScope(
permissions_data.primary_pattern, ContentSettingsPattern::Wildcard(),
permission, ContentSetting::CONTENT_SETTING_DEFAULT);
} else if (IsChooserPermissionSupported() && IsWebsiteSetting(permission)) {
hcsm()->SetWebsiteSettingDefaultScope(
permissions_data.primary_pattern.ToRepresentativeUrl(), GURL(),
permission, base::Value());
} else {
NOTREACHED() << "Unable to find ContentSettingsType in neither "
<< "ContentSettingsRegistry nor WebsiteSettingsRegistry: "
<< ConvertContentSettingsTypeToKey(permission);
}
}
is_unused_site_revocation_running_ = false;
StorePermissionInUnusedSitePermissionSetting(
unused_site_permission_types, permissions_data.chooser_permissions_data,
permissions_data.constraints.Clone(), permissions_data.primary_pattern,
ContentSettingsPattern::Wildcard());
}
void UnusedSitePermissionsManager::
DeletePatternFromRevokedUnusedSitePermissionList(
const ContentSettingsPattern& primary_pattern,
const ContentSettingsPattern& secondary_pattern) {
hcsm()->SetWebsiteSettingCustomScope(
primary_pattern, secondary_pattern,
ContentSettingsType::REVOKED_UNUSED_SITE_PERMISSIONS, {});
}
void UnusedSitePermissionsManager::StorePermissionInUnusedSitePermissionSetting(
const std::set<ContentSettingsType>& permissions,
const base::Value::Dict& chooser_permissions_data,
const std::optional<content_settings::ContentSettingConstraints> constraint,
const ContentSettingsPattern& primary_pattern,
const ContentSettingsPattern& secondary_pattern) {
const std::set<ContentSettingsType>& unused_site_permission_types =
GetRevokedUnusedSitePermissionTypes(permissions);
GURL url = GURL(primary_pattern.ToString());
CHECK(url.is_valid());
base::Value cur_value(hcsm()->GetWebsiteSetting(
url, url, ContentSettingsType::REVOKED_UNUSED_SITE_PERMISSIONS));
base::Value::Dict dict = cur_value.is_dict() ? std::move(cur_value.GetDict())
: base::Value::Dict();
base::Value::List permission_type_list =
dict.FindList(permissions::kRevokedKey)
? std::move(*dict.FindList(permissions::kRevokedKey))
: base::Value::List();
for (const auto& permission : unused_site_permission_types) {
CHECK(IsContentSetting(permission) || !IsChooserPermissionSupported() ||
chooser_permissions_data.contains(
ConvertContentSettingsTypeToKey(permission)));
permission_type_list.Append(ConvertContentSettingsTypeToKey(permission));
}
dict.Set(permissions::kRevokedKey,
base::Value::List(std::move(permission_type_list)));
if (IsChooserPermissionSupported() && !chooser_permissions_data.empty()) {
base::Value::Dict existing_chooser_permissions_data =
dict.FindDict(permissions::kRevokedChooserPermissionsKey)
? std::move(
*dict.FindDict(permissions::kRevokedChooserPermissionsKey))
: base::Value::Dict();
for (auto data : chooser_permissions_data) {
CHECK(permissions.contains(ConvertKeyToContentSettingsType(data.first)));
existing_chooser_permissions_data.Set(data.first, data.second.Clone());
}
dict.Set(permissions::kRevokedChooserPermissionsKey,
base::Value::Dict(std::move(existing_chooser_permissions_data)));
}
content_settings::ContentSettingConstraints default_constraint(clock_->Now());
default_constraint.set_lifetime(safety_hub_util::GetCleanUpThreshold());
hcsm()->SetWebsiteSettingCustomScope(
primary_pattern, secondary_pattern,
ContentSettingsType::REVOKED_UNUSED_SITE_PERMISSIONS,
base::Value(std::move(dict)),
constraint.has_value() ? constraint.value() : default_constraint);
}
void UnusedSitePermissionsManager::SetClockForTesting(base::Clock* clock) {
clock_ = clock;
}
std::vector<ContentSettingEntry>
UnusedSitePermissionsManager::GetTrackedUnusedPermissionsForTesting() {
std::vector<ContentSettingEntry> result;
for (const auto& list : recently_unused_permissions_) {
for (const auto& entry : list.second) {
result.push_back(entry);
}
}
return result;
}
void UnusedSitePermissionsManager::IgnoreOriginForAutoRevocation(
const url::Origin& origin) {
auto* registry = content_settings::ContentSettingsRegistry::GetInstance();
for (const content_settings::ContentSettingsInfo* info : *registry) {
ContentSettingsType type = info->website_settings_info()->type();
for (const auto& setting : hcsm()->GetSettingsForOneType(type)) {
if (setting.metadata.last_visited() != base::Time() &&
setting.primary_pattern.MatchesSingleOrigin() &&
setting.primary_pattern.Matches(origin.GetURL())) {
hcsm()->ResetLastVisitedTime(setting.primary_pattern,
setting.secondary_pattern, type);
break;
}
}
}
}
void UnusedSitePermissionsManager::UpdateIntegerValuesToGroupName() {
ContentSettingsForOneType settings = hcsm()->GetSettingsForOneType(
ContentSettingsType::REVOKED_UNUSED_SITE_PERMISSIONS);
bool successful_migration = true;
for (const auto& revoked_permissions : settings) {
const base::Value& stored_value = revoked_permissions.setting_value;
CHECK(stored_value.is_dict());
base::Value updated_dict(stored_value.Clone());
const base::Value::List* permission_value_list =
stored_value.GetDict().FindList(permissions::kRevokedKey);
if (permission_value_list) {
base::Value::List updated_permission_value_list =
ConvertContentSettingsIntValuesToString(
permission_value_list->Clone(), &successful_migration);
updated_dict.GetDict().Set(permissions::kRevokedKey,
std::move(updated_permission_value_list));
}
const base::Value::Dict* chooser_permission_value_dict =
stored_value.GetDict().FindDict(
permissions::kRevokedChooserPermissionsKey);
if (chooser_permission_value_dict) {
base::Value::Dict updated_chooser_permission_value_dict =
ConvertChooserContentSettingsIntValuesToString(
chooser_permission_value_dict->Clone());
updated_dict.GetDict().Set(
permissions::kRevokedChooserPermissionsKey,
std::move(updated_chooser_permission_value_dict));
}
base::Time creation_time = revoked_permissions.metadata.expiration() -
revoked_permissions.metadata.lifetime();
content_settings::ContentSettingConstraints constraints(creation_time);
constraints.set_lifetime(revoked_permissions.metadata.lifetime());
hcsm()->SetWebsiteSettingCustomScope(
revoked_permissions.primary_pattern,
revoked_permissions.secondary_pattern,
ContentSettingsType::REVOKED_UNUSED_SITE_PERMISSIONS,
std::move(updated_dict), constraints);
}
if (successful_migration) {
pref_change_registrar_->prefs()->SetBoolean(
safety_hub_prefs::kUnusedSitePermissionsRevocationMigrationCompleted,
true);
}
}