blob: bdfeee2b010f5bea2749aec2e4118f4d7df6e9b1 [file] [log] [blame]
// Copyright 2023 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/permissions/one_time_permissions_tracker.h"
#include <utility>
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/notreached.h"
#include "base/observer_list.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "chrome/browser/permissions/one_time_permissions_tracker_observer.h"
#include "chrome/browser/profiles/profile.h"
#include "components/content_settings/core/common/content_settings_types.h"
#include "components/permissions/features.h"
#include "components/permissions/permission_context_base.h"
#include "content/public/browser/visibility.h"
#include "url/gurl.h"
OneTimePermissionsTracker::OneTimePermissionsTracker() = default;
OneTimePermissionsTracker::~OneTimePermissionsTracker() = default;
OneTimePermissionsTracker::OriginTrackEntry::OriginTrackEntry() = default;
OneTimePermissionsTracker::OriginTrackEntry::~OriginTrackEntry() = default;
void OneTimePermissionsTracker::Shutdown() {
for (auto& observer : observer_list_) {
observer.OnShutdown();
}
observer_list_.Clear();
}
void OneTimePermissionsTracker::AddObserver(
OneTimePermissionsTrackerObserver* observer) {
observer_list_.AddObserver(observer);
}
void OneTimePermissionsTracker::RemoveObserver(
OneTimePermissionsTrackerObserver* observer) {
observer_list_.RemoveObserver(observer);
}
void OneTimePermissionsTracker::WebContentsBackgrounded(
const url::Origin& origin) {
if (!ShouldIgnoreOrigin(origin)) {
origin_tracker_[origin].background_tab_counter++;
if (AreAllTabsToOriginBackgroundedOrDiscarded(origin)) {
// When all undiscarded tabs which point to the origin are in the
// background, the timers should be reset.
origin_tracker_[origin].background_expiration_timer->Start(
FROM_HERE,
permissions::feature_params::kOneTimePermissionTimeout.Get(),
base::BindOnce(
&OneTimePermissionsTracker::NotifyBackgroundTimerExpired,
weak_factory_.GetWeakPtr(), origin,
OneTimePermissionsTrackerObserver::BackgroundExpiryType::
kTimeout));
origin_tracker_[origin].background_expiration_long_timer->Start(
FROM_HERE,
permissions::feature_params::kOneTimePermissionLongTimeout.Get(),
base::BindOnce(
&OneTimePermissionsTracker::NotifyBackgroundTimerExpired,
weak_factory_.GetWeakPtr(), origin,
OneTimePermissionsTrackerObserver::BackgroundExpiryType::
kLongTimeout));
HandleUserMediaState(origin, ContentSettingsType::MEDIASTREAM_CAMERA);
HandleUserMediaState(origin, ContentSettingsType::MEDIASTREAM_MIC);
} else {
origin_tracker_[origin].background_expiration_timer->Stop();
origin_tracker_[origin].background_expiration_long_timer->Stop();
}
}
}
void OneTimePermissionsTracker::WebContentsUnbackgrounded(
const url::Origin& origin) {
if (!ShouldIgnoreOrigin(origin)) {
origin_tracker_[origin].background_tab_counter--;
// Since the tab has been unbackgrounded, the timers should be reset
origin_tracker_[origin].background_expiration_timer->Stop();
origin_tracker_[origin].background_expiration_long_timer->Stop();
}
}
void OneTimePermissionsTracker::WebContentsLoadedOrigin(
const url::Origin& origin) {
if (!ShouldIgnoreOrigin(origin)) {
origin_tracker_[origin].undiscarded_tab_counter++;
origin_tracker_[origin].background_expiration_timer->Stop();
origin_tracker_[origin].background_expiration_long_timer->Stop();
}
}
void OneTimePermissionsTracker::WebContentsUnloadedOrigin(
const url::Origin& origin) {
if (!ShouldIgnoreOrigin(origin)) {
origin_tracker_[origin].undiscarded_tab_counter--;
DCHECK(!(origin_tracker_[origin].undiscarded_tab_counter < 0));
if (origin_tracker_[origin].undiscarded_tab_counter == 0) {
NotifyLastPageFromOriginClosed(origin);
}
}
}
void OneTimePermissionsTracker::StartContentSpecificExpirationTimer(
const url::Origin& origin,
ContentSettingsType content_setting,
NotifyFunction notify_callback) {
origin_tracker_[origin].used_content_settings_set.insert(content_setting);
origin_tracker_[origin]
.content_setting_specific_expiration_timer_map[content_setting]
->Start(
FROM_HERE,
permissions::feature_params::kOneTimePermissionTimeout.Get(),
base::BindOnce(notify_callback, weak_factory_.GetWeakPtr(), origin));
}
void OneTimePermissionsTracker::HandleUserMediaState(
const url::Origin& origin,
ContentSettingsType content_setting) {
NotifyFunction notify_callback;
switch (content_setting) {
case ContentSettingsType::MEDIASTREAM_CAMERA:
notify_callback = &OneTimePermissionsTracker::NotifyCapturingVideoExpired;
break;
case ContentSettingsType::MEDIASTREAM_MIC:
notify_callback = &OneTimePermissionsTracker::NotifyCapturingAudioExpired;
break;
default:
NOTREACHED();
return;
}
if (origin_tracker_[origin].used_content_settings_set.find(content_setting) !=
origin_tracker_[origin].used_content_settings_set.end()) {
if (origin_tracker_[origin]
.content_setting_specific_expiration_timer_map.find(
content_setting) ==
origin_tracker_[origin]
.content_setting_specific_expiration_timer_map.end()) {
origin_tracker_[origin]
.content_setting_specific_expiration_timer_map[content_setting] =
std::make_unique<base::OneShotTimer>();
}
if (origin_tracker_[origin]
.content_setting_specific_counter_map[content_setting] == 0 &&
AreAllTabsToOriginBackgroundedOrDiscarded(origin)) {
StartContentSpecificExpirationTimer(origin, content_setting,
notify_callback);
} else {
origin_tracker_[origin]
.content_setting_specific_expiration_timer_map[content_setting]
->Stop();
}
}
}
void OneTimePermissionsTracker::CapturingVideoChanged(const url::Origin& origin,
bool is_capturing_video) {
if (!ShouldIgnoreOrigin(origin)) {
if (is_capturing_video &&
origin_tracker_[origin].used_content_settings_set.find(
ContentSettingsType::MEDIASTREAM_CAMERA) ==
origin_tracker_[origin].used_content_settings_set.end()) {
origin_tracker_[origin].used_content_settings_set.insert(
ContentSettingsType::MEDIASTREAM_CAMERA);
}
origin_tracker_[origin].content_setting_specific_counter_map
[ContentSettingsType::MEDIASTREAM_CAMERA] +=
is_capturing_video ? 1 : -1;
HandleUserMediaState(origin, ContentSettingsType::MEDIASTREAM_CAMERA);
}
}
void OneTimePermissionsTracker::CapturingAudioChanged(const url::Origin& origin,
bool is_capturing_audio) {
if (!ShouldIgnoreOrigin(origin)) {
if (is_capturing_audio &&
origin_tracker_[origin].used_content_settings_set.find(
ContentSettingsType::MEDIASTREAM_MIC) ==
origin_tracker_[origin].used_content_settings_set.end()) {
origin_tracker_[origin].used_content_settings_set.insert(
ContentSettingsType::MEDIASTREAM_MIC);
}
origin_tracker_[origin].content_setting_specific_counter_map
[ContentSettingsType::MEDIASTREAM_MIC] += is_capturing_audio ? 1 : -1;
HandleUserMediaState(origin, ContentSettingsType::MEDIASTREAM_MIC);
}
}
void OneTimePermissionsTracker::CleanupStateForExpiredContentSetting(
ContentSettingsType type,
ContentSettingsPattern primary_pattern,
ContentSettingsPattern secondary_pattern) {
std::vector<url::Origin> affected_origins;
for (const auto& entry : origin_tracker_) {
const GURL top_level_origin_as_gurl = entry.first.GetURL();
if (primary_pattern.Matches(top_level_origin_as_gurl) &&
secondary_pattern.Matches(top_level_origin_as_gurl)) {
affected_origins.push_back(entry.first);
}
}
for (const auto& origin : affected_origins) {
if (type == ContentSettingsType::GEOLOCATION) {
origin_tracker_[origin].background_expiration_timer->Stop();
} else {
origin_tracker_[origin]
.content_setting_specific_expiration_timer_map.erase(type);
}
origin_tracker_[origin].content_setting_specific_counter_map.erase(type);
origin_tracker_[origin].used_content_settings_set.erase(type);
}
}
void OneTimePermissionsTracker::FireRunningTimersForTesting() {
// The loops in this method require manual 'forward-looking' iteration because
// the methods executing upon timer expiration might erase elements from the
// map that is being iterated over.
for (auto i = origin_tracker_.begin(), e = origin_tracker_.end(); i != e;) {
auto origin_entry = i++;
if (origin_entry->second.background_expiration_timer->IsRunning()) {
origin_entry->second.background_expiration_timer->FireNow();
}
}
for (auto i_outer = origin_tracker_.begin(), e_outer = origin_tracker_.end();
i_outer != e_outer;) {
auto origin_entry = i_outer++;
for (auto
i_inner =
origin_entry->second
.content_setting_specific_expiration_timer_map.begin(),
e_inner = origin_entry->second
.content_setting_specific_expiration_timer_map.end();
i_inner != e_inner;) {
auto timer_entry = i_inner++;
if (timer_entry->second->IsRunning()) {
timer_entry->second->FireNow();
}
}
}
}
void OneTimePermissionsTracker::NotifyLastPageFromOriginClosed(
const url::Origin& origin) {
for (auto& observer : observer_list_) {
observer.OnLastPageFromOriginClosed(origin);
}
}
bool OneTimePermissionsTracker::ShouldIgnoreOrigin(const url::Origin& origin) {
// There are cases where chrome://newtab/ and chrome://new-tab-page/ are
// used synonymously causing inconsistencies in the map. So we just ignore
// them.
return origin.opaque() ||
origin == url::Origin::Create(GURL("chrome://newtab/")) ||
origin == url::Origin::Create(GURL("chrome://new-tab-page/"));
}
void OneTimePermissionsTracker::NotifyBackgroundTimerExpired(
const url::Origin& origin,
const OneTimePermissionsTrackerObserver::BackgroundExpiryType&
expiry_type) {
for (auto& observer : observer_list_) {
observer.OnAllTabsInBackgroundTimerExpired(origin, expiry_type);
}
switch (expiry_type) {
case OneTimePermissionsTrackerObserver::BackgroundExpiryType::kTimeout:
origin_tracker_[origin].background_expiration_timer->Stop();
return;
case OneTimePermissionsTrackerObserver::BackgroundExpiryType::kLongTimeout:
origin_tracker_[origin].background_expiration_long_timer->Stop();
return;
}
}
void OneTimePermissionsTracker::NotifyCapturingVideoExpired(
const url::Origin& origin) {
for (auto& observer : observer_list_) {
observer.OnCapturingVideoExpired(origin);
}
}
void OneTimePermissionsTracker::NotifyCapturingAudioExpired(
const url::Origin& origin) {
for (auto& observer : observer_list_) {
observer.OnCapturingAudioExpired(origin);
}
}
bool OneTimePermissionsTracker::AreAllTabsToOriginBackgroundedOrDiscarded(
const url::Origin& origin) {
return origin_tracker_[origin].background_tab_counter ==
origin_tracker_[origin].undiscarded_tab_counter;
}