| // Copyright 2016 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/notifications/persistent_notification_handler.h" |
| |
| #include "base/check_op.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/content_settings/host_content_settings_map_factory.h" |
| #include "chrome/browser/lifetime/browser_shutdown.h" |
| #include "chrome/browser/lifetime/termination_notification.h" |
| #include "chrome/browser/notifications/metrics/notification_metrics_logger.h" |
| #include "chrome/browser/notifications/metrics/notification_metrics_logger_factory.h" |
| #include "chrome/browser/notifications/notification_common.h" |
| #include "chrome/browser/notifications/notification_permission_context.h" |
| #include "chrome/browser/notifications/platform_notification_service_factory.h" |
| #include "chrome/browser/notifications/platform_notification_service_impl.h" |
| #include "chrome/browser/optimization_guide/optimization_guide_keyed_service.h" |
| #include "chrome/browser/optimization_guide/optimization_guide_keyed_service_factory.h" |
| #include "chrome/browser/permissions/notifications_engagement_service_factory.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/safe_browsing/notification_content_detection/notification_content_detection_util.h" |
| #include "chrome/browser/ui/safety_hub/disruptive_notification_permissions_manager.h" |
| #include "components/content_settings/core/browser/host_content_settings_map.h" |
| #include "components/content_settings/core/common/content_settings.h" |
| #include "components/content_settings/core/common/content_settings_pattern.h" |
| #include "components/content_settings/core/common/content_settings_types.h" |
| #include "components/permissions/features.h" |
| #include "components/permissions/permission_uma_util.h" |
| #include "components/permissions/permission_util.h" |
| #include "components/safe_browsing/content/browser/notification_content_detection/notification_content_detection_constants.h" |
| #include "components/safe_browsing/core/common/features.h" |
| #include "components/site_engagement/content/site_engagement_service.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/notification_event_dispatcher.h" |
| #include "content/public/browser/permission_controller.h" |
| #include "content/public/browser/permission_descriptor_util.h" |
| #include "content/public/browser/permission_result.h" |
| #include "content/public/browser/platform_notification_context.h" |
| #include "content/public/browser/storage_partition.h" |
| #include "content/public/common/persistent_notification_status.h" |
| #include "services/metrics/public/cpp/ukm_recorder.h" |
| #include "third_party/blink/public/common/permissions/permission_utils.h" |
| #include "url/gurl.h" |
| #include "url/origin.h" |
| |
| #if BUILDFLAG(ENABLE_BACKGROUND_MODE) |
| #include "chrome/browser/profiles/keep_alive/scoped_profile_keep_alive.h" |
| #include "components/keep_alive_registry/scoped_keep_alive.h" |
| #endif // BUILDFLAG(ENABLE_BACKGROUND_MODE) |
| |
| using content::BrowserThread; |
| |
| namespace { |
| |
| void RecordCloseResult(content::PersistentNotificationStatus status) { |
| base::UmaHistogramEnumeration( |
| "Notifications.PersistentWebNotificationCloseResult", status); |
| } |
| |
| } // namespace |
| |
| PersistentNotificationHandler::PersistentNotificationHandler() { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| on_app_terminating_subscription_ = |
| browser_shutdown::AddAppTerminatingCallback( |
| base::BindOnce(&PersistentNotificationHandler::OnAppTerminating, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| PersistentNotificationHandler::~PersistentNotificationHandler() { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| } |
| |
| void PersistentNotificationHandler::OnClose( |
| Profile* profile, |
| const GURL& origin, |
| const std::string& notification_id, |
| bool by_user, |
| base::OnceClosure completed_closure) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| DCHECK(origin.is_valid()); |
| |
| if (browser_shutdown::HasShutdownStarted() || |
| g_browser_process->IsShuttingDown()) { |
| // Do not prolong shutdown by running the 'notificationclose' event. |
| RecordCloseResult( |
| content::PersistentNotificationStatus::kCanceledByAppTerminating); |
| std::move(completed_closure).Run(); |
| return; |
| } |
| |
| // TODO(peter): Should we do permission checks prior to forwarding to the |
| // NotificationEventDispatcher? |
| |
| // If we programmatically closed this notification, don't dispatch any event. |
| // |
| // TODO(crbug.com/352329050): there are circular dependencies between |
| // NotificationMetricsLogger and PlatformNotificationService. Since the |
| // service are only created lazily, and creation fails after the shutdown |
| // phase, it is possible for the factory to return null. In that case, the |
| // notification cannot have been closed programmatically. |
| if (PlatformNotificationServiceImpl* service = |
| PlatformNotificationServiceFactory::GetForProfile(profile); |
| service && service->WasClosedProgrammatically(notification_id)) { |
| std::move(completed_closure).Run(); |
| return; |
| } |
| |
| #if BUILDFLAG(ENABLE_BACKGROUND_MODE) |
| close_event_keep_alive_state_.AddKeepAlive(profile); |
| #endif // BUILDFLAG(ENABLE_BACKGROUND_MODE) |
| |
| NotificationMetricsLogger* metrics_logger = |
| NotificationMetricsLoggerFactory::GetForBrowserContext(profile); |
| if (by_user) |
| metrics_logger->LogPersistentNotificationClosedByUser(); |
| else |
| metrics_logger->LogPersistentNotificationClosedProgrammatically(); |
| |
| int32_t callback_id = close_completed_callbacks_.Add( |
| std::make_unique<base::OnceClosure>(std::move(completed_closure))); |
| |
| content::NotificationEventDispatcher::GetInstance() |
| ->DispatchNotificationCloseEvent( |
| profile, notification_id, origin, by_user, |
| base::BindOnce(&PersistentNotificationHandler::OnCloseCompleted, |
| weak_ptr_factory_.GetWeakPtr(), profile, callback_id)); |
| } |
| |
| void PersistentNotificationHandler::OnCloseCompleted( |
| Profile* profile, |
| uint64_t close_completed_callback_id, |
| content::PersistentNotificationStatus status) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| base::OnceClosure* completed_closure_pointer = |
| close_completed_callbacks_.Lookup(close_completed_callback_id); |
| if (!completed_closure_pointer) { |
| // `OnAppTerminating()` already ran the callback. |
| return; |
| } |
| |
| base::OnceClosure completed_closure = std::move(*completed_closure_pointer); |
| close_completed_callbacks_.Remove(close_completed_callback_id); |
| |
| RecordCloseResult(status); |
| |
| #if BUILDFLAG(ENABLE_BACKGROUND_MODE) |
| close_event_keep_alive_state_.RemoveKeepAlive(profile); |
| #endif // BUILDFLAG(ENABLE_BACKGROUND_MODE) |
| |
| std::move(completed_closure).Run(); |
| } |
| |
| void PersistentNotificationHandler::OnClick( |
| Profile* profile, |
| const GURL& origin, |
| const std::string& notification_id, |
| const std::optional<int>& action_index, |
| const std::optional<std::u16string>& reply, |
| base::OnceClosure completed_closure) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| NotificationMetricsLogger* metrics_logger = |
| NotificationMetricsLoggerFactory::GetForBrowserContext(profile); |
| |
| #if BUILDFLAG(ENABLE_BACKGROUND_MODE) |
| click_event_keep_alive_state_.AddKeepAlive(profile); |
| #endif // BUILDFLAG(ENABLE_BACKGROUND_MODE) |
| |
| blink::mojom::PermissionStatus permission_status = |
| profile->GetPermissionController() |
| ->GetPermissionResultForOriginWithoutContext( |
| content::PermissionDescriptorUtil:: |
| CreatePermissionDescriptorForPermissionType( |
| blink::PermissionType::NOTIFICATIONS), |
| url::Origin::Create(origin)) |
| .status; |
| |
| // Don't process click events when the |origin| doesn't have permission. This |
| // can't be a DCHECK because of potential races with native notifications. |
| if (permission_status != blink::mojom::PermissionStatus::GRANTED) { |
| metrics_logger->LogPersistentNotificationClickWithoutPermission(); |
| |
| OnClickCompleted(profile, notification_id, std::move(completed_closure), |
| content::PersistentNotificationStatus::kPermissionMissing); |
| return; |
| } |
| |
| if (action_index.has_value()) |
| metrics_logger->LogPersistentNotificationActionButtonClick(); |
| else |
| metrics_logger->LogPersistentNotificationClick(); |
| |
| // TODO(crbug.com/40280229) |
| if (!origin.is_empty()) { |
| auto* service = |
| NotificationsEngagementServiceFactory::GetForProfile(profile); |
| // This service might be missing for incognito profiles and in tests. |
| if (service) { |
| service->RecordNotificationInteraction(origin); |
| } |
| |
| // Notification clicks are considered a form of engagement with the |
| // |origin|, thus we log the interaction with the Site Engagement service. |
| site_engagement::SiteEngagementService::Get(profile) |
| ->HandleNotificationInteraction(origin); |
| } |
| |
| content::NotificationEventDispatcher::GetInstance() |
| ->DispatchNotificationClickEvent( |
| profile, notification_id, origin, action_index, reply, |
| base::BindOnce(&PersistentNotificationHandler::OnClickCompleted, |
| weak_ptr_factory_.GetWeakPtr(), profile, |
| notification_id, std::move(completed_closure))); |
| |
| // If there is a proposed disruptive notification revocation, report a false |
| // positive due to user interacting with a notification. Disruptive are |
| // notifications with high notification volume and low site engagement score. |
| ukm::SourceId source_id = ukm::UkmRecorder::GetSourceIdForNotificationEvent( |
| base::PassKey<PersistentNotificationHandler>(), origin); |
| DisruptiveNotificationPermissionsManager::MaybeReportFalsePositive( |
| profile, origin, |
| DisruptiveNotificationPermissionsManager::FalsePositiveReason:: |
| kPersistentNotificationClick, |
| source_id); |
| } |
| |
| void PersistentNotificationHandler::OnClickCompleted( |
| Profile* profile, |
| const std::string& notification_id, |
| base::OnceClosure completed_closure, |
| content::PersistentNotificationStatus status) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| UMA_HISTOGRAM_ENUMERATION( |
| "Notifications.PersistentWebNotificationClickResult", status); |
| |
| switch (status) { |
| case content::PersistentNotificationStatus::kSuccess: |
| case content::PersistentNotificationStatus::kServiceWorkerError: |
| case content::PersistentNotificationStatus::kWaitUntilRejected: |
| // There either wasn't a failure, or one that's in the developer's |
| // control, so we don't act on the origin's behalf. |
| break; |
| case content::PersistentNotificationStatus::kServiceWorkerMissing: |
| case content::PersistentNotificationStatus::kDatabaseError: |
| case content::PersistentNotificationStatus::kPermissionMissing: |
| // There was a failure that's out of the developer's control. The user now |
| // observes a stuck notification, so let's close it for them. |
| PlatformNotificationServiceFactory::GetForProfile(profile) |
| ->ClosePersistentNotification(notification_id); |
| break; |
| |
| case content::PersistentNotificationStatus::kCanceledByAppTerminating: |
| // App termination must not cancel the click event. |
| NOTREACHED(); |
| } |
| |
| #if BUILDFLAG(ENABLE_BACKGROUND_MODE) |
| click_event_keep_alive_state_.RemoveKeepAlive(profile); |
| #endif // BUILDFLAG(ENABLE_BACKGROUND_MODE) |
| |
| std::move(completed_closure).Run(); |
| } |
| |
| void PersistentNotificationHandler::OnAppTerminating() { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| // Release all keep alives for currently running 'notificationclose' events. |
| // This will allow browser shutdown to begin without waiting for the |
| // 'notificationclose' events to complete. |
| #if BUILDFLAG(ENABLE_BACKGROUND_MODE) |
| close_event_keep_alive_state_.RemoveAllKeepAlives(); |
| #endif |
| |
| // Like `OnCloseCompleted()` above, run the 'notificationclose' completed |
| // callbacks after removing the keep alives. |
| for (CloseCompletedCallbackMap::iterator it(&close_completed_callbacks_); |
| !it.IsAtEnd(); it.Advance()) { |
| RecordCloseResult( |
| content::PersistentNotificationStatus::kCanceledByAppTerminating); |
| std::move(*it.GetCurrentValue()).Run(); |
| } |
| close_completed_callbacks_.Clear(); |
| } |
| |
| void PersistentNotificationHandler::DisableNotifications(Profile* profile, |
| const GURL& origin) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| permissions::PermissionUmaUtil::ScopedRevocationReporter |
| scoped_revocation_reporter( |
| profile, origin, origin, ContentSettingsType::NOTIFICATIONS, |
| permissions::PermissionSourceUI::INLINE_SETTINGS); |
| #if BUILDFLAG(IS_ANDROID) |
| // On Android, NotificationChannelsProviderAndroid does not support moving a |
| // channel from ALLOW to BLOCK state, so simply delete the channel instead. |
| NotificationPermissionContext::UpdatePermission(profile, origin, |
| CONTENT_SETTING_DEFAULT); |
| #else |
| NotificationPermissionContext::UpdatePermission(profile, origin, |
| CONTENT_SETTING_BLOCK); |
| #endif |
| // Remove `origin` from user allowlisted sites when user unsubscribes. |
| auto* hcsm = HostContentSettingsMapFactory::GetForProfile(profile); |
| if (hcsm && origin.is_valid()) { |
| hcsm->SetWebsiteSettingCustomScope( |
| ContentSettingsPattern::FromURLNoWildcard(origin), |
| ContentSettingsPattern::Wildcard(), |
| ContentSettingsType::ARE_SUSPICIOUS_NOTIFICATIONS_ALLOWLISTED_BY_USER, |
| base::Value(base::Value::Dict().Set( |
| safe_browsing::kIsAllowlistedByUserKey, false))); |
| } |
| } |
| |
| void PersistentNotificationHandler::OpenSettings(Profile* profile, |
| const GURL& origin) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| NotificationCommon::OpenNotificationSettings(profile, origin); |
| } |
| |
| void PersistentNotificationHandler::ReportNotificationAsSafe( |
| const std::string& notification_id, |
| const GURL& url, |
| Profile* profile) { |
| OnMaybeReport(notification_id, url, profile, /*did_show_warning=*/true, |
| /*did_user_unsubscribe=*/false); |
| } |
| |
| void PersistentNotificationHandler::ReportWarnedNotificationAsSpam( |
| const std::string& notification_id, |
| const GURL& url, |
| Profile* profile) { |
| OnMaybeReport(notification_id, url, profile, /*did_show_warning=*/true, |
| /*did_user_unsubscribe=*/true); |
| } |
| |
| void PersistentNotificationHandler::ReportUnwarnedNotificationAsSpam( |
| const std::string& notification_id, |
| const GURL& url, |
| Profile* profile) { |
| OnMaybeReport(notification_id, url, profile, /*did_show_warning=*/false, |
| /*did_user_unsubscribe=*/true); |
| } |
| |
| void PersistentNotificationHandler::OnMaybeReport( |
| const std::string& notification_id, |
| const GURL& url, |
| Profile* profile, |
| bool did_show_warning, |
| bool did_user_unsubscribe) { |
| CHECK(profile); |
| |
| // In case the data volume becomes excessive, logging should happen at a |
| // sampled rate. This rate is defined by the |
| // `kReportNotificationContentDetectionDataRate` feature parameter. |
| if (base::RandDouble() * 100 > |
| safe_browsing::kReportNotificationContentDetectionDataRate.Get()) { |
| return; |
| } |
| |
| scoped_refptr<content::PlatformNotificationContext> notification_context = |
| profile->GetStoragePartitionForUrl(url)->GetPlatformNotificationContext(); |
| if (!notification_context || |
| !OptimizationGuideKeyedServiceFactory::GetForProfile(profile)) { |
| return; |
| } |
| |
| blink::mojom::EngagementLevel engagement_level = |
| blink::mojom::EngagementLevel::NONE; |
| if (site_engagement::SiteEngagementService::Get(profile)) { |
| engagement_level = site_engagement::SiteEngagementService::Get(profile) |
| ->GetEngagementLevel(url); |
| } |
| |
| // Read notification data from database and upload as log to model quality |
| // service. |
| notification_context->ReadNotificationDataAndRecordInteraction( |
| notification_id, url, |
| content::PlatformNotificationContext::Interaction::NONE, |
| base::BindOnce( |
| &safe_browsing::SendNotificationContentDetectionDataToMQLSServer, |
| OptimizationGuideKeyedServiceFactory::GetForProfile(profile) |
| ->GetModelQualityLogsUploaderService() |
| ->GetWeakPtr(), |
| safe_browsing::NotificationContentDetectionMQLSMetadata( |
| did_show_warning, did_user_unsubscribe, engagement_level))); |
| } |
| |
| #if BUILDFLAG(ENABLE_BACKGROUND_MODE) |
| |
| PersistentNotificationHandler::NotificationKeepAliveState:: |
| NotificationKeepAliveState(KeepAliveOrigin keep_alive_origin, |
| ProfileKeepAliveOrigin profile_keep_alive_origin) |
| : keep_alive_origin_(keep_alive_origin), |
| profile_keep_alive_origin_(profile_keep_alive_origin) {} |
| |
| PersistentNotificationHandler::NotificationKeepAliveState:: |
| ~NotificationKeepAliveState() = default; |
| |
| void PersistentNotificationHandler::NotificationKeepAliveState::AddKeepAlive( |
| Profile* profile) { |
| // Ensure the browser and Profile stay alive while the event is processed. The |
| // keep alives will be reset when all events have been acknowledged. |
| if (pending_dispatch_events_++ == 0) { |
| event_dispatch_keep_alive_ = std::make_unique<ScopedKeepAlive>( |
| keep_alive_origin_, KeepAliveRestartOption::DISABLED); |
| } |
| // TODO(crbug.com/40159237): Remove IsOffTheRecord() when Incognito profiles |
| // support refcounting. |
| if (!profile->IsOffTheRecord() && |
| profile_pending_dispatch_events_[profile]++ == 0) { |
| event_dispatch_profile_keep_alives_[profile] = |
| std::make_unique<ScopedProfileKeepAlive>(profile, |
| profile_keep_alive_origin_); |
| } |
| } |
| |
| void PersistentNotificationHandler::NotificationKeepAliveState::RemoveKeepAlive( |
| Profile* profile) { |
| DCHECK_GT(pending_dispatch_events_, 0); |
| // Reset the keep alive if all in-flight events have been processed. |
| if (--pending_dispatch_events_ == 0) |
| event_dispatch_keep_alive_.reset(); |
| |
| // TODO(crbug.com/40159237): Remove IsOffTheRecord() when Incognito profiles |
| // support refcounting. |
| if (!profile->IsOffTheRecord() && |
| --profile_pending_dispatch_events_[profile] == 0) { |
| event_dispatch_profile_keep_alives_[profile].reset(); |
| } |
| } |
| |
| void PersistentNotificationHandler::NotificationKeepAliveState:: |
| RemoveAllKeepAlives() { |
| event_dispatch_keep_alive_.reset(); |
| event_dispatch_profile_keep_alives_.clear(); |
| pending_dispatch_events_ = 0; |
| profile_pending_dispatch_events_.clear(); |
| } |
| |
| #endif // BUILDFLAG(ENABLE_BACKGROUND_MODE) |