blob: 4fffb7d65f1bef698a3e8c013cd7ce17bbbdcb94 [file] [log] [blame]
// Copyright 2022 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/ui/safety_hub/unused_site_permissions_service.h"
#include <memory>
#include <string>
#include "base/check.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/json/values_util.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/field_trial_params.h"
#include "base/metrics/histogram_functions.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/time/clock.h"
#include "base/time/default_clock.h"
#include "base/time/time.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/ui/safety_hub/safety_hub_service.h"
#include "chrome/common/chrome_features.h"
#include "chrome/grit/generated_resources.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_utils.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "components/content_settings/core/browser/website_settings_info.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/content_settings_utils.h"
#include "components/content_settings/core/common/features.h"
#include "components/permissions/constants.h"
#include "components/permissions/permission_uma_util.h"
#include "components/permissions/pref_names.h"
#include "components/prefs/pref_change_registrar.h"
#include "components/prefs/pref_service.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "ui/base/l10n/l10n_util.h"
#include "url/gurl.h"
#include "url/origin.h"
constexpr base::TimeDelta kRevocationThresholdNoDelayForTesting = base::Days(0);
constexpr base::TimeDelta kRevocationThresholdWithDelayForTesting =
base::Minutes(5);
constexpr base::TimeDelta kRevocationCleanUpThresholdWithDelayForTesting =
base::Minutes(30);
namespace {
// Reflects the maximum number of days between a permissions being revoked and
// the time when the user regrants the permission through the unused site
// permission module of Safete Check. The maximum number of days is determined
// by `kRevocationCleanUpThreshold`.
size_t kAllowAgainMetricsExclusiveMaxCount = 31;
// Using a single bucket per day, following the value of
// |kAllowAgainMetricsExclusiveMaxCount|.
size_t kAllowAgainMetricsBuckets = 31;
base::TimeDelta GetRevocationThreshold() {
// TODO(crbug.com/40250875): Clean up no delay revocation after the feature is
// ready. Today, no delay revocation is necessary to enable manual testing.
if (content_settings::features::kSafetyCheckUnusedSitePermissionsNoDelay
.Get()) {
return kRevocationThresholdNoDelayForTesting;
} else if (content_settings::features::
kSafetyCheckUnusedSitePermissionsWithDelay.Get()) {
return kRevocationThresholdWithDelayForTesting;
}
return content_settings::features::
kSafetyCheckUnusedSitePermissionsRevocationThreshold.Get();
}
base::TimeDelta GetCleanUpThreshold() {
// TODO(crbug.com/40250875): Clean up delayed clean up logic after the feature
// is ready. Today, this is necessary to enable manual testing.
if (content_settings::features::kSafetyCheckUnusedSitePermissionsWithDelay
.Get()) {
return kRevocationCleanUpThresholdWithDelayForTesting;
}
return content_settings::features::
kSafetyCheckUnusedSitePermissionsRevocationCleanUpThreshold.Get();
}
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);
}
std::string ConvertContentSettingsTypeToKey(ContentSettingsType type) {
return base::NumberToString(static_cast<int32_t>(type));
}
ContentSettingsType ConvertKeyToContentSettingsType(std::string key) {
int number = -1;
DCHECK(base::StringToInt(key, &number));
return static_cast<ContentSettingsType>(number);
}
} // namespace
base::TimeDelta UnusedSitePermissionsService::GetRepeatedUpdateInterval() {
return content_settings::features::
kSafetyCheckUnusedSitePermissionsRepeatedUpdateInterval.Get();
}
UnusedSitePermissionsService::TabHelper::TabHelper(
content::WebContents* web_contents,
UnusedSitePermissionsService* unused_site_permission_service)
: content::WebContentsObserver(web_contents),
content::WebContentsUserData<TabHelper>(*web_contents),
unused_site_permission_service_(
unused_site_permission_service->AsWeakPtr()) {}
UnusedSitePermissionsService::TabHelper::~TabHelper() = default;
PermissionsData::PermissionsData() = default;
PermissionsData::~PermissionsData() = default;
PermissionsData::PermissionsData(const PermissionsData& other)
: origin(other.origin),
permission_types(other.permission_types),
constraints(other.constraints) {
chooser_permissions_data = other.chooser_permissions_data.Clone();
}
UnusedSitePermissionsService::UnusedSitePermissionsResult::
UnusedSitePermissionsResult() = default;
UnusedSitePermissionsService::UnusedSitePermissionsResult::
~UnusedSitePermissionsResult() = default;
UnusedSitePermissionsService::UnusedSitePermissionsResult::
UnusedSitePermissionsResult(const UnusedSitePermissionsResult&) = default;
std::unique_ptr<SafetyHubService::Result>
UnusedSitePermissionsService::UnusedSitePermissionsResult::Clone() const {
return std::make_unique<UnusedSitePermissionsResult>(*this);
}
void UnusedSitePermissionsService::UnusedSitePermissionsResult::
AddRevokedPermission(const PermissionsData& permissions_data) {
revoked_permissions_.push_back(std::move(permissions_data));
}
std::list<PermissionsData> UnusedSitePermissionsService::
UnusedSitePermissionsResult::GetRevokedPermissions() {
std::list<PermissionsData> result(revoked_permissions_);
return result;
}
std::set<ContentSettingsPattern>
UnusedSitePermissionsService::UnusedSitePermissionsResult::GetRevokedOrigins()
const {
std::set<ContentSettingsPattern> origins;
for (auto permission : revoked_permissions_) {
origins.insert(permission.origin);
}
return origins;
}
base::Value::Dict
UnusedSitePermissionsService::UnusedSitePermissionsResult::ToDictValue() const {
base::Value::Dict result = BaseToDictValue();
base::Value::List revoked_origins;
for (auto permission : revoked_permissions_) {
revoked_origins.Append(permission.origin.ToString());
}
result.Set(kUnusedSitePermissionsResultKey, std::move(revoked_origins));
return result;
}
bool UnusedSitePermissionsService::UnusedSitePermissionsResult::
IsTriggerForMenuNotification() const {
// A menu notification should be shown when there is at least one permission
// that was revoked.
return !GetRevokedOrigins().empty();
}
bool UnusedSitePermissionsService::UnusedSitePermissionsResult::
WarrantsNewMenuNotification(
const base::Value::Dict& previous_result_dict) const {
std::set<ContentSettingsPattern> old_origins;
for (const base::Value& origin_val :
*previous_result_dict.FindList(kUnusedSitePermissionsResultKey)) {
// Before crrev.com/c/5000387, the revoked permissions were stored in a dict
// that looked as follows:
// {
// "origin": "site.com",
// "permissionTypes": [...permissions],
// "expiration": TimeValue
// }
// After this CL, the list was updated to a list of strings representing
// the origins. To maintain backwards compatibility, we should support these
// old values for now. This check can be deleted in the future.
const std::string* origin_str{};
if (origin_val.is_dict()) {
const base::Value::Dict& revoked_permission = origin_val.GetDict();
origin_str = revoked_permission.FindString(kSafetyHubOriginKey);
} else if (origin_val.is_string()) {
origin_str = &origin_val.GetString();
} else {
NOTREACHED();
}
ContentSettingsPattern origin =
ContentSettingsPattern::FromString(*origin_str);
old_origins.insert(origin);
}
std::set<ContentSettingsPattern> new_origins = GetRevokedOrigins();
return !base::ranges::includes(old_origins, new_origins);
}
std::u16string UnusedSitePermissionsService::UnusedSitePermissionsResult::
GetNotificationString() const {
#if !BUILDFLAG(IS_ANDROID)
if (revoked_permissions_.empty()) {
return std::u16string();
}
return l10n_util::GetPluralStringFUTF16(
IDS_SETTINGS_SAFETY_HUB_UNUSED_SITE_PERMISSIONS_MENU_NOTIFICATION,
revoked_permissions_.size());
#else
// Menu notifications are not present on Android.
return std::u16string();
#endif
}
int UnusedSitePermissionsService::UnusedSitePermissionsResult::
GetNotificationCommandId() const {
return IDC_OPEN_SAFETY_HUB;
}
void UnusedSitePermissionsService::TabHelper::PrimaryPageChanged(
content::Page& page) {
if (unused_site_permission_service_) {
unused_site_permission_service_->OnPageVisited(
page.GetMainDocument().GetLastCommittedOrigin());
}
}
WEB_CONTENTS_USER_DATA_KEY_IMPL(UnusedSitePermissionsService::TabHelper);
UnusedSitePermissionsService::UnusedSitePermissionsService(
content::BrowserContext* browser_context,
PrefService* prefs)
: browser_context_(browser_context),
clock_(base::DefaultClock::GetInstance()) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK(browser_context_);
content_settings_observation_.Observe(hcsm());
if (base::FeatureList::IsEnabled(features::kSafetyHub)) {
pref_change_registrar_ = std::make_unique<PrefChangeRegistrar>();
pref_change_registrar_->Init(prefs);
pref_change_registrar_->Add(
permissions::prefs::kUnusedSitePermissionsRevocationEnabled,
base::BindRepeating(&UnusedSitePermissionsService::
OnPermissionsAutorevocationControlChanged,
base::Unretained(this)));
}
InitializeLatestResult();
if (!IsAutoRevocationEnabled()) {
return;
}
StartRepeatedUpdates();
}
UnusedSitePermissionsService::~UnusedSitePermissionsService() = default;
std::unique_ptr<SafetyHubService::Result>
UnusedSitePermissionsService::InitializeLatestResultImpl() {
return GetRevokedPermissions();
}
void UnusedSitePermissionsService::OnContentSettingChanged(
const ContentSettingsPattern& primary_pattern,
const ContentSettingsPattern& secondary_pattern,
ContentSettingsTypeSet content_type_set) {
if (content_type_set.Contains(
ContentSettingsType::REVOKED_UNUSED_SITE_PERMISSIONS)) {
return;
}
// When permissions change for a pattern it is either (1) through resetting
// permissions, e.g. in page info or site settings, (2) user modifying
// permissions manually, or (3) through the auto-revocation that this module
// performs. In (1) and (2) the pattern should no longer be shown to the user.
// 1: After resetting permissions the browser state should be in a
// state as if the permission had never been granted.
// 2: The user is actively engaging with the permissions of the site, so it is
// no longer considered an unused site for the purposes of this module.
// This includes the case where unrelated permissions to the revoked ones
// are changed.
// 3: Current logic ensures this does not happen for sites that already have
// revoked permissions. This module revokes permissions in an all-or-none
// fashion.
DeletePatternFromRevokedPermissionList(primary_pattern, secondary_pattern);
}
void UnusedSitePermissionsService::Shutdown() {
content_settings_observation_.Reset();
}
void UnusedSitePermissionsService::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);
for (auto& permission_type : *permission_type_list) {
// Check if stored permission type is valid.
auto type_int = permission_type.GetIfInt();
if (!type_int.has_value()) {
continue;
}
// Look up ContentSettingsRegistry to see if type is content setting
// or website setting.
auto type = static_cast<ContentSettingsType>(type_int.value());
if (IsContentSetting(type)) {
// ContentSettingsRegistry-based permissions with ALLOW value were
// revoked; re-grant them by setting ALLOW again.
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);
// There should be always data attached for a revoked chooser permission.
DCHECK(chooser_permissions_data);
// Chooser permissions are WebsiteSettingsRegistry-based, so it is
// re-granted by restoring the previously revoked Website Setting value.
auto* revoked_value = chooser_permissions_data->FindDict(
ConvertContentSettingsTypeToKey(type));
DCHECK(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);
}
}
// Ignore origin from future auto-revocations.
IgnoreOriginForAutoRevocation(origin);
// Remove origin from revoked permissions list.
DeletePatternFromRevokedPermissionList(info.primary_pattern,
info.secondary_pattern);
// Record the days elapsed from auto-revocation to regrant.
base::Time revoked_time =
info.metadata.expiration() -
content_settings::features::
kSafetyCheckUnusedSitePermissionsRevocationCleanUpThreshold.Get();
base::UmaHistogramCustomCounts(
"Settings.SafetyCheck.UnusedSitePermissionsAllowAgainDays",
(clock_->Now() - revoked_time).InDays(), 0,
kAllowAgainMetricsExclusiveMaxCount, kAllowAgainMetricsBuckets);
}
void UnusedSitePermissionsService::UndoRegrantPermissionsForOrigin(
const PermissionsData& permissions_data) {
for (const auto& permission : permissions_data.permission_types) {
if (IsContentSetting(permission)) {
hcsm()->SetContentSettingCustomScope(
permissions_data.origin, ContentSettingsPattern::Wildcard(),
permission, ContentSetting::CONTENT_SETTING_DEFAULT);
} else if (IsChooserPermissionSupported() && IsWebsiteSetting(permission)) {
hcsm()->SetWebsiteSettingDefaultScope(
permissions_data.origin.ToRepresentativeUrl(), GURL(), permission,
base::Value());
} else {
NOTREACHED() << "Unable to find ContentSettingsType in neither "
<< "ContentSettingsRegistry nor WebsiteSettingsRegistry: "
<< ConvertContentSettingsTypeToKey(permission);
}
}
StorePermissionInRevokedPermissionSetting(
permissions_data.permission_types,
permissions_data.chooser_permissions_data, permissions_data.constraints,
permissions_data.origin, ContentSettingsPattern::Wildcard());
}
void UnusedSitePermissionsService::ClearRevokedPermissionsList() {
for (const auto& revoked_permissions : hcsm()->GetSettingsForOneType(
ContentSettingsType::REVOKED_UNUSED_SITE_PERMISSIONS)) {
DeletePatternFromRevokedPermissionList(
revoked_permissions.primary_pattern,
revoked_permissions.secondary_pattern);
}
}
void UnusedSitePermissionsService::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;
}
}
}
}
// Called by TabHelper when a URL was visited.
void UnusedSitePermissionsService::OnPageVisited(const url::Origin& origin) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
// Check if this origin has unused permissions.
auto origin_entry = recently_unused_permissions_.find(origin.Serialize());
if (origin_entry == recently_unused_permissions_.end()) {
return;
}
// See which permissions of the origin actually match the URL and update them.
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++;
}
}
// Remove origin entry if all permissions were updated.
if (site_permissions.empty()) {
recently_unused_permissions_.erase(origin_entry);
}
}
void UnusedSitePermissionsService::DeletePatternFromRevokedPermissionList(
const ContentSettingsPattern& primary_pattern,
const ContentSettingsPattern& secondary_pattern) {
hcsm()->SetWebsiteSettingCustomScope(
primary_pattern, secondary_pattern,
ContentSettingsType::REVOKED_UNUSED_SITE_PERMISSIONS, {});
}
base::OnceCallback<std::unique_ptr<SafetyHubService::Result>()>
UnusedSitePermissionsService::GetBackgroundTask() {
return base::BindOnce(&UnusedSitePermissionsService::UpdateOnBackgroundThread,
clock_, base::WrapRefCounted(hcsm()));
}
std::unique_ptr<SafetyHubService::Result>
UnusedSitePermissionsService::UpdateOnBackgroundThread(
base::Clock* clock,
const scoped_refptr<HostContentSettingsMap> hcsm) {
UnusedSitePermissionsService::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) {
// Skip wildcard patterns that don't belong to a single origin. These
// shouldn't track visit timestamps.
if (!setting.primary_pattern.MatchesSingleOrigin()) {
continue;
}
if (setting.metadata.last_visited() != base::Time() &&
setting.metadata.last_visited() < threshold) {
GURL url = GURL(setting.primary_pattern.ToString());
// Converting URL to a origin is normally an anti-pattern but here it is
// ok since the URL belongs to a single origin. Therefore, it has a
// fully defined URL+scheme+port which makes converting URL to origin
// successful.
url::Origin origin = url::Origin::Create(url);
recently_unused[origin.Serialize()].push_back(
{type, std::move(setting)});
}
}
}
auto result = std::make_unique<
UnusedSitePermissionsService::UnusedSitePermissionsResult>();
result->SetRecentlyUnusedPermissions(recently_unused);
return std::move(result);
}
std::unique_ptr<SafetyHubService::Result>
UnusedSitePermissionsService::UpdateOnUIThread(
std::unique_ptr<SafetyHubService::Result> result) {
auto* interim_result =
static_cast<UnusedSitePermissionsService::UnusedSitePermissionsResult*>(
result.get());
recently_unused_permissions_ = interim_result->GetRecentlyUnusedPermissions();
RevokeUnusedPermissions();
return GetRevokedPermissions();
}
std::unique_ptr<UnusedSitePermissionsService::Result>
UnusedSitePermissionsService::GetRevokedPermissions() {
ContentSettingsForOneType settings = hcsm()->GetSettingsForOneType(
ContentSettingsType::REVOKED_UNUSED_SITE_PERMISSIONS);
auto result = std::make_unique<
UnusedSitePermissionsService::UnusedSitePermissionsResult>();
for (const auto& revoked_permissions : settings) {
PermissionsData permissions_data;
permissions_data.origin = revoked_permissions.primary_pattern;
const base::Value& stored_value = revoked_permissions.setting_value;
DCHECK(stored_value.is_dict());
const base::Value::List* type_list =
stored_value.GetDict().FindList(permissions::kRevokedKey);
CHECK(type_list);
for (base::Value& type : type_list->Clone()) {
permissions_data.permission_types.insert(
static_cast<ContentSettingsType>(type.GetInt()));
}
permissions_data.constraints = content_settings::ContentSettingConstraints(
revoked_permissions.metadata.expiration() -
revoked_permissions.metadata.lifetime());
permissions_data.constraints.set_lifetime(
revoked_permissions.metadata.lifetime());
result->AddRevokedPermission(permissions_data);
}
return result;
}
void UnusedSitePermissionsService::RevokeUnusedPermissions() {
if (!IsAutoRevocationEnabled()) {
return;
}
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;
// All |primary_pattern|s are equal across list items, the same is true for
// |secondary_pattern|s. This property is needed later and checked in the
// loop.
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;
// Check if the current permission can be auto revoked.
if (!content_settings::CanBeAutoRevoked(entry.type,
entry.source.setting_value)) {
permission_itr++;
continue;
}
DCHECK_EQ(entry.source.primary_pattern, primary_pattern);
DCHECK(entry.source.secondary_pattern ==
ContentSettingsPattern::Wildcard() ||
entry.source.secondary_pattern == entry.source.primary_pattern);
// Reset the permission to default if the site is visited before
// threshold. Also, the secondary pattern should be wildcard.
DCHECK_NE(entry.source.metadata.last_visited(), base::Time());
DCHECK(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);
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++;
}
}
// Store revoked permissions on HCSM.
if (!revoked_permissions.empty()) {
StorePermissionInRevokedPermissionSetting(
revoked_permissions, chooser_permissions_data, std::nullopt,
primary_pattern, secondary_pattern);
}
// Handle clean up of recently_unused_permissions_ map after revocation.
if (unused_site_permissions.empty()) {
// Since all unused permissions are revoked, the map should be cleared.
recently_unused_permissions_.erase(itr++);
} else {
// Since there are some permissions that are not revoked, the tracked
// unused permissions should be set to those permissions.
// Note that, currently all permissions belong to a single domain will
// revoked all together, since triggering permission prompt requires a
// page visit. So the timestamp of all granted permissions of the origin
// will be updated. However, this logic will prevent edge cases like
// permission prompt stays open long time, also will provide support for
// revoking permissions separately in the future.
itr++;
}
}
}
void UnusedSitePermissionsService::StorePermissionInRevokedPermissionSetting(
const PermissionsData& permissions_data) {
// The |secondary_pattern| for
// |ContentSettingsType::REVOKED_UNUSED_SITE_PERMISSIONS| is always wildcard.
StorePermissionInRevokedPermissionSetting(
permissions_data.permission_types,
permissions_data.chooser_permissions_data, permissions_data.constraints,
permissions_data.origin, ContentSettingsPattern::Wildcard());
}
void UnusedSitePermissionsService::StorePermissionInRevokedPermissionSetting(
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) {
GURL url = GURL(primary_pattern.ToString());
// The url should be valid as it is checked that the pattern represents a
// single origin.
DCHECK(url.is_valid());
// Get the current value of the setting to append the recently revoked
// permissions.
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 : permissions) {
// Chooser permissions (not ContentSettingsRegistry-based) should have
// corresponding data to be restored in `chooser_permissions_data`.
DCHECK(IsContentSetting(permission) || !IsChooserPermissionSupported() ||
chooser_permissions_data.contains(
ConvertContentSettingsTypeToKey(permission)));
permission_type_list.Append(static_cast<int32_t>(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) {
// Chooser permissions data should have its permission type included in
// `permissions` set.
DCHECK(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(GetCleanUpThreshold());
// Set website setting for the list of recently revoked permissions and
// previously revoked permissions, if exists.
hcsm()->SetWebsiteSettingCustomScope(
primary_pattern, secondary_pattern,
ContentSettingsType::REVOKED_UNUSED_SITE_PERMISSIONS,
base::Value(std::move(dict)),
constraint.has_value() ? constraint.value() : default_constraint);
}
void UnusedSitePermissionsService::OnPermissionsAutorevocationControlChanged() {
if (IsAutoRevocationEnabled()) {
StartRepeatedUpdates();
} else {
StopTimer();
}
}
std::vector<UnusedSitePermissionsService::ContentSettingEntry>
UnusedSitePermissionsService::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 UnusedSitePermissionsService::SetClockForTesting(base::Clock* clock) {
clock_ = clock;
}
base::WeakPtr<SafetyHubService> UnusedSitePermissionsService::GetAsWeakRef() {
return weak_factory_.GetWeakPtr();
}
bool UnusedSitePermissionsService::IsAutoRevocationEnabled() {
// If kSafetyHub is disabled, then the auto-revocation directly depends on
// kSafetyCheckUnusedSitePermissions.
if (!base::FeatureList::IsEnabled(features::kSafetyHub)) {
return base::FeatureList::IsEnabled(
content_settings::features::kSafetyCheckUnusedSitePermissions);
}
return pref_change_registrar_->prefs()->GetBoolean(
permissions::prefs::kUnusedSitePermissionsRevocationEnabled);
}