// 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/content_settings/one_time_permission_provider.h"

#include <algorithm>
#include <memory>
#include <set>

#include "base/power_monitor/power_monitor.h"
#include "base/scoped_observation.h"
#include "base/time/default_clock.h"
#include "base/time/time.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/browser_process_platform_part.h"
#include "chrome/browser/permissions/one_time_permissions_tracker.h"
#include "chrome/browser/permissions/one_time_permissions_tracker_factory.h"
#include "chrome/browser/permissions/one_time_permissions_tracker_observer.h"
#include "components/content_settings/core/browser/content_settings_registry.h"
#include "components/content_settings/core/browser/content_settings_rule.h"
#include "components/content_settings/core/browser/permission_settings_registry.h"
#include "components/content_settings/core/browser/user_modifiable_provider.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_metadata.h"
#include "components/content_settings/core/common/content_settings_utils.h"
#include "components/content_settings/core/common/features.h"
#include "components/permissions/permission_uma_util.h"
#include "components/permissions/permission_util.h"
#include "url/gurl.h"

class OneTimePermissionProvider::TrackerObserver
    : public OneTimePermissionsTrackerObserver {
 public:
  TrackerObserver(OneTimePermissionProvider* provider,
                  OneTimePermissionsTracker* tracker)
      : provider_(provider) {
    observation_.Observe(tracker);
  }
  ~TrackerObserver() override = default;

  // OneTimePermissionsTrackerObserver:
  void OnLastPageFromOriginClosed(const url::Origin& origin) override {
    provider_->OnLastPageFromOriginClosed(origin);
  }

  void OnAllTabsInBackgroundTimerExpired(
      const url::Origin& origin,
      const BackgroundExpiryType& expiry_type) override {
    provider_->OnAllTabsInBackgroundTimerExpired(
        origin, expiry_type == BackgroundExpiryType::kLongTimeout);
  }

  void OnCapturingVideoExpired(const url::Origin& origin) override {
    provider_->OnCapturingVideoExpired(origin);
  }

  void OnCapturingAudioExpired(const url::Origin& origin) override {
    provider_->OnCapturingAudioExpired(origin);
  }

  void OnShutdown() override {
    observation_.Reset();
    provider_->OnShutdown();
  }

 private:
  raw_ptr<OneTimePermissionProvider> provider_;
  base::ScopedObservation<OneTimePermissionsTracker,
                          OneTimePermissionsTrackerObserver>
      observation_{this};
};

OneTimePermissionProvider::OneTimePermissionProvider(
    OneTimePermissionsTracker* one_time_permissions_tracker)
    : one_time_permissions_tracker_(one_time_permissions_tracker),
      tracker_observer_(
          std::make_unique<TrackerObserver>(this,
                                            one_time_permissions_tracker_)),
      clock_(base::DefaultClock::GetInstance()) {
  // The PowerMonitor is initialized in content_main_runner_impl.cc before the
  // main function for the browser process is run (which initializes the HCSM).
  // For this reason, the PowerMonitor is always initialized before the observer
  // is added here.
  base::PowerMonitor::GetInstance()->AddPowerSuspendObserver(this);
}

OneTimePermissionProvider::~OneTimePermissionProvider() {
  base::PowerMonitor::GetInstance()->RemovePowerSuspendObserver(this);
}

std::unique_ptr<content_settings::RuleIterator>
OneTimePermissionProvider::GetRuleIterator(ContentSettingsType content_type,
                                           bool incognito) const {
  if (!permissions::PermissionUtil::DoesStoreTemporaryGrantsInHcsm(
          content_type)) {
    return nullptr;
  }
  return value_map_.GetRuleIterator(content_type);
}

std::unique_ptr<content_settings::Rule> OneTimePermissionProvider::GetRule(
    const GURL& primary_url,
    const GURL& secondary_url,
    ContentSettingsType content_type,
    bool off_the_record) const {
  if (!permissions::PermissionUtil::DoesStoreTemporaryGrantsInHcsm(
          content_type)) {
    return nullptr;
  }

  base::AutoLock auto_lock(value_map_.GetLock());
  return value_map_.GetRule(primary_url, secondary_url, content_type);
}

bool OneTimePermissionProvider::SetWebsiteSetting(
    const ContentSettingsPattern& primary_pattern,
    const ContentSettingsPattern& secondary_pattern,
    ContentSettingsType content_settings_type,
    const base::Value& value,
    const content_settings::ContentSettingConstraints& constraints) {
  if (!permissions::PermissionUtil::DoesStoreTemporaryGrantsInHcsm(
          content_settings_type)) {
    return false;
  }

  auto* info = content_settings::PermissionSettingsRegistry::GetInstance()->Get(
      content_settings_type);
  if (!info) {
    // Object permissions cannot be mapped to a PermissionSetting and thus
    // cannot be handled by this provider.
    return false;
  }

  auto setting = info->delegate().FromValue(value);

  // For transitions to block and resetting to default, clear the grant and let
  // the pref provider handle the permission as usual.
  if (!setting || info->delegate().IsBlocked(*setting)) {
    {
      base::AutoLock lock(value_map_.GetLock());
      auto* previous_one_time_grant = value_map_.GetValue(
          primary_pattern.ToRepresentativeUrl(),
          secondary_pattern.ToRepresentativeUrl(), content_settings_type);

      if (!previous_one_time_grant) {
        // If there was no grant, it means that this content setting is
        // already being handled by the pref provider.
        return false;
      }

      value_map_.DeleteValue(primary_pattern, secondary_pattern,
                             content_settings_type);
    }

    NotifyObservers(primary_pattern, secondary_pattern, content_settings_type);

    permissions::PermissionUmaUtil::RecordOneTimePermissionEvent(
        content_settings_type,
        permissions::OneTimePermissionEvent::REVOKED_MANUALLY);

    return false;
  }

  if (constraints.session_model() !=
      content_settings::mojom::SessionModel::ONE_TIME) {
    // Changes to the durable permission should reset the corresponding one-time
    // grant. This is triggered for example by a transition from Allow once to
    // Allow.
    base::AutoLock lock(value_map_.GetLock());
    value_map_.DeleteValue(primary_pattern, secondary_pattern,
                           content_settings_type);

    return false;
  }

  CHECK(info->delegate().IsAnyPermissionAllowed(*setting))
      << "One time permission grants should allow something.";

  base::Time now = clock_->Now();
  content_settings::RuleMetaData metadata;
  metadata.set_session_model(content_settings::mojom::SessionModel::ONE_TIME);
  metadata.set_last_modified(now);
  if (content_settings::ShouldTypeExpireActively(content_settings_type) &&
      !constraints.lifetime().is_zero()) {
    metadata.SetExpirationAndLifetime(now + constraints.lifetime(),
                                      constraints.lifetime());
  }

  permissions::PermissionUmaUtil::RecordOneTimePermissionEvent(
      content_settings_type,
      permissions::OneTimePermissionEvent::GRANTED_ONE_TIME);

  {
    base::AutoLock lock(value_map_.GetLock());
    value_map_.SetValue(primary_pattern, secondary_pattern,
                        content_settings_type, value.Clone(),
                        std::move(metadata));
  }

  NotifyObservers(primary_pattern, secondary_pattern, content_settings_type);

  // Changes to one-time grants might need updates in the pref provider.
  return false;
}

bool OneTimePermissionProvider::UpdateLastUsedTime(
    const GURL& primary_url,
    const GURL& secondary_url,
    ContentSettingsType content_type,
    const base::Time time) {
  // Last used time is not tracked for one-time permissions.
  return false;
}

bool OneTimePermissionProvider::SetAutorevocationBypassedByUser(
    const ContentSettingsPattern& primary_pattern,
    const ContentSettingsPattern& secondary_pattern,
    ContentSettingsType content_type) {
  // Autorevocation does not include one-time permissions.
  return false;
}

bool OneTimePermissionProvider::UpdateLastVisitTime(
    const ContentSettingsPattern& primary_pattern,
    const ContentSettingsPattern& secondary_pattern,
    ContentSettingsType content_type) {
  // LastVisit time is not tracked for one-time permissions.
  return false;
}

std::optional<base::TimeDelta> OneTimePermissionProvider::RenewContentSetting(
    const GURL& primary_url,
    const GURL& secondary_url,
    ContentSettingsType type,
    std::optional<ContentSetting> setting_to_match) {
  // Setting renewal is not supported for one-time permissions.
  return std::nullopt;
}

void OneTimePermissionProvider::ClearAllContentSettingsRules(
    ContentSettingsType content_type) {
  if (!permissions::PermissionUtil::DoesStoreTemporaryGrantsInHcsm(
          content_type)) {
    return;
  }
  base::AutoLock lock(value_map_.GetLock());
  value_map_.DeleteValues(content_type);
}

void OneTimePermissionProvider::ShutdownOnUIThread() {
  RemoveAllObservers();
}

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

void OneTimePermissionProvider::ExpireWebsiteSetting(
    const ContentSettingsPattern& primary_pattern,
    const ContentSettingsPattern& secondary_pattern,
    ContentSettingsType content_settings_type) {
  // Custom scope because NotifyObservers also requires value_map_'s exclusive
  // lock.
  {
    base::AutoLock lock(value_map_.GetLock());
    if (value_map_.GetValue(primary_pattern.ToRepresentativeUrl(),
                            secondary_pattern.ToRepresentativeUrl(),
                            content_settings_type) == nullptr) {
      return;
    }

    value_map_.DeleteValue(primary_pattern, secondary_pattern,
                           content_settings_type);
  }
  permissions::PermissionUmaUtil::RecordOneTimePermissionEvent(
      content_settings_type,
      permissions::OneTimePermissionEvent::EXPIRED_AFTER_MAXIMUM_LIFETIME);
  NotifyObservers(primary_pattern, secondary_pattern, content_settings_type);
}

void OneTimePermissionProvider::OnSuspend() {
  std::vector<ContentSettingEntry> patterns_to_delete;
  auto* registry = content_settings::ContentSettingsRegistry::GetInstance();

  for (const auto* info : *registry) {
    auto setting_type = info->website_settings_info()->type();
    if (permissions::PermissionUtil::DoesStoreTemporaryGrantsInHcsm(
            setting_type)) {
      std::unique_ptr<content_settings::RuleIterator> rule_iterator(
          value_map_.GetRuleIterator(setting_type));

      while (rule_iterator && rule_iterator->HasNext()) {
        auto rule = rule_iterator->Next();
        patterns_to_delete.emplace_back(setting_type, rule->primary_pattern,
                                        rule->secondary_pattern);
        permissions::PermissionUmaUtil::RecordOneTimePermissionEvent(
            setting_type,
            permissions::OneTimePermissionEvent::EXPIRED_ON_SUSPEND);
      }
    }
  }

  DeleteEntriesAndNotify(patterns_to_delete);
}

// All tabs with the given origin have either been closed or navigated away
// from. We remove all permissions associated with the origin.
void OneTimePermissionProvider::OnLastPageFromOriginClosed(
    const url::Origin& origin) {
  for (auto setting_type :
       content_settings::GetTypesWithTemporaryGrantsInHcsm()) {
    DeleteEntriesMatchingGURL(
        setting_type, origin.GetURL(),
        permissions::OneTimePermissionEvent::ALL_TABS_CLOSED_OR_DISCARDED);
  }
}

// All tabs with the given origin have either been in the background for a
// certain time or not used for a certain time. This situation currently only
// expires geolocation. We remove the geolocation permission associated with
// the origin.
void OneTimePermissionProvider::OnAllTabsInBackgroundTimerExpired(
    const url::Origin& origin,
    bool is_long_timeout) {
  if (is_long_timeout) {
    return;
  }
  DeleteEntriesMatchingGURL(
      permissions::PermissionUtil::GetGeolocationType(), origin.GetURL(),
      permissions::OneTimePermissionEvent::EXPIRED_IN_BACKGROUND);
}

// All tabs to the origin have not shown a tab indicator for video for a
// certain time and have been in the background. We remove the camera
// permission associated with the origin.
void OneTimePermissionProvider::OnCapturingVideoExpired(
    const url::Origin& origin) {
  DeleteEntriesMatchingGURL(
      ContentSettingsType::MEDIASTREAM_CAMERA, origin.GetURL(),
      permissions::OneTimePermissionEvent::EXPIRED_IN_BACKGROUND);
}

// All tabs to the origin have not shown a tab indicator for microphone access
// for a certain time and have been in the background. We remove the
// microphone permission associated with the origin.
void OneTimePermissionProvider::OnCapturingAudioExpired(
    const url::Origin& origin) {
  DeleteEntriesMatchingGURL(
      ContentSettingsType::MEDIASTREAM_MIC, origin.GetURL(),
      permissions::OneTimePermissionEvent::EXPIRED_IN_BACKGROUND);
}

void OneTimePermissionProvider::DeleteEntriesAndNotify(
    const std::vector<OneTimePermissionProvider::ContentSettingEntry>&
        entries_to_delete) {
  {
    base::AutoLock lock(value_map_.GetLock());
    for (const auto& pattern : entries_to_delete) {
      value_map_.DeleteValue(pattern.primary_pattern, pattern.secondary_pattern,
                             pattern.type);

      // In all the observable `OneTimePermissionsTrackerObserver` events
      // which are emitted from the tracker, the tracker knows for which
      // origins and content settings it can halt bookkeeping. However, the
      // tracker isn't aware of externally deleted content settings. To
      // prevent it from triggering observers for an already deleted content
      // setting, we need to inform it about the deletion here (and only
      // here).
      if (one_time_permissions_tracker_) {
        one_time_permissions_tracker_->CleanupStateForExpiredContentSetting(
            pattern.type, pattern.primary_pattern, pattern.secondary_pattern);
      }
    }
  }

  for (const auto& pattern : entries_to_delete) {
    NotifyObservers(pattern.primary_pattern, pattern.secondary_pattern,
                    pattern.type);
  }
}

void OneTimePermissionProvider::DeleteEntriesMatchingGURL(
    ContentSettingsType content_setting_type,
    const GURL& origin_gurl,
    permissions::OneTimePermissionEvent trigger_event) {
  std::vector<ContentSettingEntry> patterns_to_delete;
  std::unique_ptr<content_settings::RuleIterator> rule_iterator(
      value_map_.GetRuleIterator(content_setting_type));

  while (rule_iterator && rule_iterator->HasNext()) {
    auto rule = rule_iterator->Next();
    if (rule->primary_pattern.Matches(origin_gurl) &&
        rule->secondary_pattern.Matches(origin_gurl)) {
      patterns_to_delete.emplace_back(
          content_setting_type, rule->primary_pattern, rule->secondary_pattern);
      permissions::PermissionUmaUtil::RecordOneTimePermissionEvent(
          content_setting_type, trigger_event);
    }
  }

  rule_iterator.reset();

  DeleteEntriesAndNotify(patterns_to_delete);
}

void OneTimePermissionProvider::OnShutdown() {
  one_time_permissions_tracker_ = nullptr;
}
