// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef CHROME_BROWSER_UI_SAFETY_HUB_ABUSIVE_NOTIFICATION_PERMISSIONS_MANAGER_H_
#define CHROME_BROWSER_UI_SAFETY_HUB_ABUSIVE_NOTIFICATION_PERMISSIONS_MANAGER_H_

#include "base/gtest_prod_util.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "chrome/browser/ui/safety_hub/safety_hub_constants.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "components/content_settings/core/common/content_settings.h"
#include "components/safe_browsing/core/browser/db/database_manager.h"
#include "components/safe_browsing/core/browser/safe_browsing_metrics_collector.h"

class GURL;
class Profile;

namespace safe_browsing {
class V5GetHashProtocolManager;
}

namespace {
// Maximum time in milliseconds to wait for the Safe Browsing service reputation
// check. After this amount of time the outstanding check will be aborted, and
// the resource will be treated as if it were safe.
inline constexpr int kCheckUrlTimeoutMs = 5000;

// Key of the base::Value dictionary we assign to the
// REVOKED_ABUSIVE_NOTIFICATION_PERMISSION to specify revocation reason.
inline constexpr char kAbusiveRevocationSourceKeyStr[] = "revocation_source";
inline constexpr char kSocialEngineeringBlocklistStr[] = "social_engineering";
inline constexpr char kSafeBrowsingUnwantedRevocationStr[] = "manual";
inline constexpr char kSuspiciousContentAutoRevocationStr[] =
    "suspicious_content";
}  // namespace

// This class keeps track of abusive notification permissions by checking URLs
// against the Safe Browsing social engineering blocklist. This also handles
// automatic revocation and responding to user decisions in Safety Hub.
class AbusiveNotificationPermissionsManager {
 public:
  // These values are persisted to logs. Entries should not be renumbered and
  // numeric values should never be reused.
  //
  // LINT.IfChange(AbusiveNotificationPermissionsInteractions)
  enum class AbusiveNotificationPermissionsInteractions {
    kOpenReviewUI = 0,
    kAllowAgain = 1,
    kAcknowledgeAll = 2,
    kUndoAllowAgain = 3,
    kUndoAcknowledgeAll = 4,
    kMinimize = 5,
    kGoToSettings = 6,
    kMaxValue = kGoToSettings,
  };
  // LINT.ThenChange(//tools/metrics/histograms/metadata/settings/enums.xml:SafetyCheckUnusedSitePermissionsModuleInteractions)

  // LINT.IfChange(AbusiveNotificationPermissionsCheckResult)
  // These values are persisted to logs. Entries should not be renumbered and
  // numeric values should never be reused.
  enum class CheckResult {
    // The check completed and the site was safe.
    kSafe = 0,
    // The check completed and the site was classified as phishing.
    kPhishing = 1,
    // The check timed out before a result was received.
    kTimeout = 2,
    kMaxValue = kTimeout,
  };
  // LINT.ThenChange(//tools/metrics/histograms/metadata/settings/enums.xml:AbusiveNotificationPermissionsCheckResult)

  explicit AbusiveNotificationPermissionsManager(
      scoped_refptr<safe_browsing::SafeBrowsingDatabaseManager>
          database_manager,
      base::WeakPtr<safe_browsing::V5GetHashProtocolManager>
          v5_get_hash_protocol_manager,
      scoped_refptr<HostContentSettingsMap> hcsm,
      PrefService* pref_service);

  AbusiveNotificationPermissionsManager(
      const AbusiveNotificationPermissionsManager&) = delete;
  AbusiveNotificationPermissionsManager& operator=(
      const AbusiveNotificationPermissionsManager&) = delete;

  ~AbusiveNotificationPermissionsManager();

  // Handles the auto-revocation of abusive notification permissions for both
  // social engineering blocklist and manual enforcement. This includes updating
  // the settings with the correct expiration, and logging metrics.
  static void ExecuteAbusiveNotificationAutoRevocation(
      HostContentSettingsMap* hcsm,
      GURL url,
      safe_browsing::NotificationRevocationSource revocation_source,
      const raw_ptr<const base::Clock> clock);

  // Sets the `REVOKED_ABUSIVE_NOTIFICATION_PERMISSIONS` value for a url,
  // revocation source, given the constraints, and whether the user wants to
  // ignore future auto-revocation.
  static void SetRevokedAbusiveNotificationPermission(
      HostContentSettingsMap* hcsm,
      GURL url,
      bool is_ignored,
      safe_browsing::NotificationRevocationSource revocation_source,
      const content_settings::ContentSettingConstraints& constraints = {});

  // Perform `SetRevokedAbusiveNotificationPermission`, preserving revocation
  // source if exists.
  static void SetRevokedAbusiveNotificationPermission(
      HostContentSettingsMap* hcsm,
      GURL url,
      bool is_ignored,
      const content_settings::ContentSettingConstraints& constraints = {});

  // Revoke notification permission for `url` if suspicious notification
  // criteria are met, setting `REVOKED_ABUSIVE_NOTIFICATION_PERMISSIONS`.
  // Return true if the notification has been revoked.
  static bool MaybeRevokeSuspiciousNotificationPermission(Profile* profile,
                                                          GURL url);

  // Return `NotificationRevocationSource` if there is  a
  // `REVOKED_ABUSIVE_NOTIFICATION_PERMISSIONS` setting value for the
  // `setting_url` with the `safety_hub::kAbusiveRevocationSourceKeyStr`.
  // Returns `NotificationRevocationSource::kUnknown` if the setting does not
  // exist, the key does not exist, or the value is invalid.
  static safe_browsing::NotificationRevocationSource
  GetRevokedAbusiveNotificationRevocationSource(HostContentSettingsMap* hcsm,
                                                GURL url);

  // Returns true if `url` belongs to a site with revoked abusive notifications
  // with `NotificationRevocationSource::kSuspiciousContentAutoRevocation` as
  // notification revocation source.
  static bool IsUrlRevokedDueToSuspiciousContent(HostContentSettingsMap* hcsm,
                                                 GURL url);

  // Calls `PerformSafeBrowsingChecks` on URLs which have notifications
  // enabled and haven't been marked as a URL to be ignored.
  void CheckNotificationPermissionOrigins();

  // If the url has a revoked abusive notification permission, this method
  // allows notification permissions again and adds a constraint so that this
  // permission is not auto-revoked during future Safety Hub checks.
  void RegrantPermissionForOriginIfNecessary(const GURL& url);

  // If `permission_types` includes notifications, undo the actions from
  // `RegrantPermissionForOrigin` by changing the `NOTIFICATIONS` setting back
  // to `CONTENT_SETTING_ASK` and the
  // `REVOKED_ABUSIVE_NOTIFICATION_PERMISSIONS` setting back to a dictionary
  // with `safety_hub::kRevokedStatusDictKeyStr` set to
  // `safety_hub::kRevokeStr`.
  void UndoRegrantPermissionForOriginIfNecessary(
      const GURL& url,
      std::set<ContentSettingsType> permission_types,
      content_settings::ContentSettingConstraints constraints);

  // Clear the list of abusive notification permissions so they will no longer
  // be shown to the user. Does not change permissions themselves.
  void ClearRevokedPermissionsList();

  // Remove the `REVOKED_ABUSIVE_NOTIFICATION_PERMISSIONS` setting for the given
  // pattern pairs.
  void DeletePatternFromRevokedAbusiveNotificationList(
      const ContentSettingsPattern& primary_pattern,
      const ContentSettingsPattern& secondary_pattern);

  // Log metric for permission changed and remove
  // `REVOKED_ABUSIVE_NOTIFICATION_PERMISSIONS` setting for the given pattern
  // pairs.
  void OnPermissionChanged(const ContentSettingsPattern& primary_pattern,
                           const ContentSettingsPattern& secondary_pattern);

  // Restores REVOKED_ABUSIVE_NOTIFICATION_PERMISSIONS entry for the
  // primary_pattern after it was deleted after user
  // has accepted the revocation (via `ClearRevokedPermissionsList()`).
  void RestoreDeletedRevokedPermission(
      const ContentSettingsPattern& primary_pattern,
      content_settings::ContentSettingConstraints constraints);

  // If there's a clock for testing, return that. Otherwise, return an instance
  // of a default clock.
  const base::Clock* GetClock();

  // Returns true if settings are being changed by
  // `AbusiveNotificationPermissionManager` activities and should be ignored by
  // `RevokedPermissionsService::OnContentSettingChanged`.
  bool IsRevocationRunning();

  // Helper method for logging the
  // `AbusiveNotificationPermissionRevocation.Interactions` UKM.
  void LogAbusiveNotificationPermissionRevocationUKM(
      const GURL& origin,
      AbusiveNotificationPermissionsInteractions interaction,
      safe_browsing::NotificationRevocationSource source);

  // Test support:
  // TODO(crbug/342210522): Use a unique_ptr here if possible.
  void SetClockForTesting(base::Clock* clock) { clock_for_testing_ = clock; }

 private:
  friend class AbusiveNotificationPermissionsManagerTest;
  friend class AbusiveNotificationPermissionsRevocationTest;
  FRIEND_TEST_ALL_PREFIXES(
      AbusiveNotificationPermissionsManagerTest,
      AddAllowedAbusiveNotificationSitesToRevokedOriginSet);
  FRIEND_TEST_ALL_PREFIXES(
      AbusiveNotificationPermissionsManagerTest,
      DoesNotAddSafeAbusiveNotificationSitesToRevokedOriginSet);
  FRIEND_TEST_ALL_PREFIXES(AbusiveNotificationPermissionsManagerTest,
                           DoesNotAddBlockedSettingToRevokedList);
  FRIEND_TEST_ALL_PREFIXES(AbusiveNotificationPermissionsManagerTest,
                           DoesNotAddIgnoredSettingToRevokedList);
  FRIEND_TEST_ALL_PREFIXES(AbusiveNotificationPermissionsManagerTest,
                           DoesNotAddAbusiveNotificationSitesOnTimeout);
  FRIEND_TEST_ALL_PREFIXES(AbusiveNotificationPermissionsManagerTest,
                           RegrantedPermissionShouldNotBeChecked);
  FRIEND_TEST_ALL_PREFIXES(AbusiveNotificationPermissionsManagerTest,
                           ClearRevokedPermissionsList);
  FRIEND_TEST_ALL_PREFIXES(AbusiveNotificationPermissionsManagerTest,
                           SetRevokedAbusiveNotificationPermission);
  FRIEND_TEST_ALL_PREFIXES(AbusiveNotificationPermissionsManagerTest,
                           SetIgnoreRevokedAbusiveNotificationPermission);
  FRIEND_TEST_ALL_PREFIXES(AbusiveNotificationPermissionsManagerTest,
                           UndoRegrantPermissionForOriginIfNecessary);

  // On object creation, checks the Safe Browsing blocklist for `url_`
  // and revokes notification permissions if blocklisted.
  // This subclass is necessary to avoid concurrency issues - each
  // request needs its own Client and the AbusiveNotificationPermissionsManager
  // makes multiple Safe Browsing requests.
  class SafeBrowsingCheckClient
      : safe_browsing::SafeBrowsingDatabaseManager::Client {
   public:
    SafeBrowsingCheckClient(
        base::PassKey<safe_browsing::SafeBrowsingDatabaseManager::Client>
            pass_key,
        safe_browsing::SafeBrowsingDatabaseManager* database_manager,
        base::WeakPtr<safe_browsing::V5GetHashProtocolManager>
            v5_get_hash_protocol_manager,
        raw_ptr<std::map<SafeBrowsingCheckClient*,
                         std::unique_ptr<SafeBrowsingCheckClient>>>
            safe_browsing_request_clients,
        raw_ptr<HostContentSettingsMap> hcsm,
        PrefService* pref_service,
        GURL url,
        int safe_browsing_check_delay,
        const base::Clock* clock);

    ~SafeBrowsingCheckClient() override;

    // Trigger the call to check the Safe Browsing social engineering blocklist.
    void CheckSocialEngineeringBlocklist();

    // safe_browsing::SafeBrowsingDatabaseManager::Client:
    base::WeakPtr<safe_browsing::V5GetHashProtocolManager>
    GetV5GetHashProtocolManager() override;

   private:
    // safe_browsing::SafeBrowsingDatabaseManager::Client:
    void OnCheckBrowseUrlResult(
        const GURL& url,
        safe_browsing::SBThreatType threat_type) override;

    // Callback to be run if a Safe Browsing blocklist request does not return
    // a response within `kCheckUrlTimeoutMs` time.
    void OnCheckBlocklistTimeout();

    // Logs the outcome of the Safe Browsing blocklist check.
    void LogCheckResult(CheckResult result);

    // A pointer to the `database_manager_` of the
    // `AbusiveNotificationPermissionsManager`.
    raw_ptr<safe_browsing::SafeBrowsingDatabaseManager> database_manager_;

    // The protocol manager used for Safe Browsing v5 get hash requests.
    base::WeakPtr<safe_browsing::V5GetHashProtocolManager>
        v5_get_hash_protocol_manager_;

    // A pointer to the `safe_browsing_request_clients_`
    // of the `AbusiveNotificationPermissionsManager`.
    raw_ptr<std::map<SafeBrowsingCheckClient*,
                     std::unique_ptr<SafeBrowsingCheckClient>>>
        safe_browsing_request_clients_;

    // A pointer to the `hcsm_` of the `AbusiveNotificationPermissionsManager`.
    raw_ptr<HostContentSettingsMap> hcsm_;

    // A pointer to the `pref_service_` of the
    // `AbusiveNotificationPermissionsManager`.
    raw_ptr<PrefService> pref_service_;

    // The URL that is being checked against the Safe Browsing blocklist.
    GURL url_;

    // Delay amount allowed for blocklist checks.
    int safe_browsing_check_delay_;

    // Timer for running Safe Browsing blocklist checks. If `kCheckUrlTimeoutMs`
    // time has passed, run `OnCheckBlocklistTimeout`.
    base::OneShotTimer timer_;

    // To enable automatic cleanup after the threshold has passed, this is used
    // to set the lifetime of the `REVOKED_ABUSIVE_NOTIFICATION_PERMISSIONS`
    // value.
    const raw_ptr<const base::Clock> clock_;

    base::WeakPtrFactory<
        AbusiveNotificationPermissionsManager::SafeBrowsingCheckClient>
        weak_factory_{this};
  };

  // Primarily used for tests.
  void SetNullSBCheckDelayForTesting() { safe_browsing_check_delay_ = 0; }

  // Create a `SafeBrowsingCheckClient` object, triggering a blocklist check,
  // and add it to `safe_browsing_request_clients_`.
  void PerformSafeBrowsingChecks(GURL url);

  // Returns true if the notification permission is allowed and the setting
  // does not indicate "ignore".
  bool ShouldCheckOrigin(const ContentSettingPatternSource& setting) const;

  // Clears any helper members that stored state from the previous safety check.
  // Called each time Safe Browsing checks are performed on a set of URLs.
  void ResetSafeBrowsingCheckHelpers();

  // Update Notification permission setting in response to Safety Hub actions
  // (e.g. re-grant and undo re-grant). This will set
  // `is_abusive_site_revocation_running_` to signal
  // `RevokedPermissionsService::OnContentSettingChanged` to ignore the setting
  // change.
  void UpdateNotificationPermissionForSafetyHubAction(
      HostContentSettingsMap* hcsm,
      GURL url,
      ContentSetting setting_value);

  // Convert `NotificationRevocationSource` to its string representation for
  // storing in `REVOKED_ABUSIVE_NOTIFICATION_PERMISSIONS`.
  static std::optional<std::string> GetRevocationSourceString(
      safe_browsing::NotificationRevocationSource source);

  // Used for interactions with the local database, when checking the blocklist.
  scoped_refptr<safe_browsing::SafeBrowsingDatabaseManager> database_manager_;

  // Object that allows us to manage site permissions.
  scoped_refptr<HostContentSettingsMap> hcsm_;

  // Used for updating prefs after performing blocklist checks.
  raw_ptr<PrefService> pref_service_;

  // The protocol manager used for Safe Browsing v5 get hash requests.
  base::WeakPtr<safe_browsing::V5GetHashProtocolManager>
      v5_get_hash_protocol_manager_;

  // Safe Browsing blocklist check clients. Each object is responsible for a
  // single Safe Browsing check, given a URL. Stored this way so that the object
  // can delete itself from this map within the SafeBrowsingCheckClient.
  std::map<SafeBrowsingCheckClient*, std::unique_ptr<SafeBrowsingCheckClient>>
      safe_browsing_request_clients_;

  // Length of time allowed for Safe Browsing check before timeout. This allows
  // us to test timeout behavior.
  int safe_browsing_check_delay_;

  // To enable automatic cleanup after the threshold has passed, this is used to
  // set the lifetime of the `REVOKED_ABUSIVE_NOTIFICATION_PERMISSIONS` value.
  // Pass this into each instance of the SafeBrowsingCheckClient class.
  raw_ptr<base::Clock> clock_for_testing_;

  // True if automatic check is occurring or
  // `AbusiveNotificationPermissionManager` is changing notification setting.
  // This value is used to help decide whether to handle setting change inside
  // `OnContentSettingChanged`.
  bool is_abusive_site_revocation_running_ = false;
};

#endif  // CHROME_BROWSER_UI_SAFETY_HUB_ABUSIVE_NOTIFICATION_PERMISSIONS_MANAGER_H_
